win_drag_source 0.0.2 copy "win_drag_source: ^0.0.2" to clipboard
win_drag_source: ^0.0.2 copied to clipboard

PlatformWindows

Native Windows OLE drag source for Flutter. Drag files or text with a custom RepaintBoundary ghost image, using pure Dart FFI.

example/lib/main.dart

import 'dart:convert';
import 'dart:io';
import 'dart:math';

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

void main() {
  runApp(const ExampleApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'win_drag_source example',
      theme: ThemeData.dark(useMaterial3: true),
      home: const HomePage(),
    );
  }
}

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  /// Last drag result + a peek at the payload that was dragged.
  String _lastStatus = '(no drag yet)';
  String _lastPayloadPeek = '';

  // ===========================================================================
  // Demo 1: file drag
  // ===========================================================================

  /// Creates a temp file on demand so the user has something real to drag.
  Future<String> _ensureDemoFile() async {
    final dir = await Directory.systemTemp.createTemp('drag_demo_');
    final file = File('${dir.path}\\demo.txt');
    await file.writeAsString(
      'hello from win_drag_source\ncreated at ${DateTime.now()}\n',
    );
    return file.path;
  }

  // ===========================================================================
  // Demo 2: encrypted payload drag
  // ===========================================================================

  /// **MOCK ONLY.** In real code, replace with AES-GCM / RSA / whatever your
  /// receiver expects. We just XOR + base64 here so the output *looks* like
  /// a ciphertext when dropped into Notepad — but it is NOT secure.
  ///
  /// A realistic payload is usually a JSON with `path`, `id`, `guid`, etc.
  /// encrypted by the sender and decrypted by the receiver.
  String _mockEncrypt(String plaintext) {
    const key = 'demo-key';
    final kBytes = key.codeUnits;
    final pBytes = utf8.encode(plaintext);
    final out = List<int>.generate(
      pBytes.length,
      (i) => pBytes[i] ^ kBytes[i % kBytes.length],
    );
    return base64Encode(out);
  }

  String _buildEncryptedPayload() {
    final plaintext = jsonEncode({
      'path': r'C:\fake\path\resource.art',
      'id': 10042,
      'guid': 'a3f0-c12d-4b88-9e17',
      'issuedAt': DateTime.now().toIso8601String(),
    });
    return _mockEncrypt(plaintext);
  }

  // ===========================================================================
  // UI helpers
  // ===========================================================================

  void _setStatus(String status, {String payloadPeek = ''}) {
    if (!mounted) return;
    setState(() {
      _lastStatus = status;
      _lastPayloadPeek = payloadPeek;
    });
  }

  String _truncate(String s, int n) =>
      s.length <= n ? s : '${s.substring(0, n)}…';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('win_drag_source example')),
      body: Center(
        child: ConstrainedBox(
          constraints: const BoxConstraints(maxWidth: 720),
          child: ListView(
            padding: const EdgeInsets.all(24),
            children: [
              _StatusPanel(
                status: _lastStatus,
                payloadPeek: _lastPayloadPeek,
              ),
              const SizedBox(height: 24),
              Text('Demos', style: Theme.of(context).textTheme.titleLarge),
              const SizedBox(height: 12),

              // ----- Demo 1: file drag -----
              _SectionHeader(
                number: 1,
                title: 'File drag (CF_HDROP)',
                description: 'Drag into Explorer, a chat app, 3ds Max, etc. '
                    'The receiver gets the actual file.',
              ),
              const SizedBox(height: 8),
              _DragCard(
                payloadProvider: () async {
                  final path = await _ensureDemoFile();
                  return FilePayload(path);
                },
                onTap: () => _setStatus('Tap on demo 1'),
                onDrop: (ok, peek) => _setStatus(
                  ok ? 'Demo 1 dropped (accepted)' : 'Demo 1 dropped (rejected/cancelled)',
                  payloadPeek: peek,
                ),
              ),

              const SizedBox(height: 24),

              // ----- Demo 2: encrypted payload drag -----
              _SectionHeader(
                number: 2,
                title: 'Encrypted payload (CF_UNICODETEXT)',
                description: 'Receivers get a base64-style ciphertext. '
                    'Plain-text targets (Notepad, chat) display it as opaque text; '
                    'your own target decrypts it. See _mockEncrypt in main.dart — '
                    'replace with AES-GCM / RSA in production.',
              ),
              const SizedBox(height: 8),
              _DragCard(
                payload: TextPayload(_buildEncryptedPayload()),
                onTap: () => _setStatus('Tap on demo 2'),
                onDrop: (ok, peek) => _setStatus(
                  ok ? 'Demo 2 dropped (accepted)' : 'Demo 2 dropped (rejected/cancelled)',
                  payloadPeek: 'ciphertext: ${_truncate(peek, 60)}',
                ),
              ),

              const SizedBox(height: 24),

              // ----- Demo 3: custom ghost image -----
              _SectionHeader(
                number: 3,
                title: 'Custom ghost image (imageKey)',
                description: 'Only the cover is rasterized as the drag preview — '
                    'badges and labels are excluded via imageKey.',
              ),
              const SizedBox(height: 8),
              _CustomGhostCard(
                onTap: () => _setStatus('Tap on demo 3'),
                onDrop: (ok) => _setStatus(
                  ok ? 'Demo 3 dropped (accepted)' : 'Demo 3 dropped (rejected/cancelled)',
                ),
              ),

              const SizedBox(height: 32),
              const Text(
                'Tip: press Escape to cancel a drag.\n'
                'Windows-only — on other platforms the cards just respond to taps.',
                style: TextStyle(color: Colors.grey),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

// =============================================================================
// UI building blocks
// =============================================================================

class _StatusPanel extends StatelessWidget {
  const _StatusPanel({required this.status, required this.payloadPeek});

  final String status;
  final String payloadPeek;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.black26,
        borderRadius: BorderRadius.circular(8),
        border: Border.all(color: Colors.white12),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('Last status',
              style: TextStyle(color: Colors.grey.shade400, fontSize: 12)),
          const SizedBox(height: 4),
          Text(status, style: const TextStyle(fontSize: 16)),
          if (payloadPeek.isNotEmpty) ...[
            const SizedBox(height: 8),
            Text('Payload',
                style: TextStyle(color: Colors.grey.shade400, fontSize: 12)),
            const SizedBox(height: 4),
            Text(payloadPeek,
                style: const TextStyle(
                  fontFamily: 'monospace',
                  fontSize: 12,
                  color: Colors.lightBlueAccent,
                )),
          ],
        ],
      ),
    );
  }
}

class _SectionHeader extends StatelessWidget {
  const _SectionHeader({
    required this.number,
    required this.title,
    required this.description,
  });

  final int number;
  final String title;
  final String description;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          children: [
            CircleAvatar(
              radius: 12,
              backgroundColor: Colors.blueGrey,
              child: Text('$number', style: const TextStyle(fontSize: 12)),
            ),
            const SizedBox(width: 8),
            Text(title,
                style: const TextStyle(
                    fontSize: 16, fontWeight: FontWeight.w600)),
          ],
        ),
        const SizedBox(height: 4),
        Text(description,
            style: TextStyle(color: Colors.grey.shade400, fontSize: 13)),
      ],
    );
  }
}

class _DragCard extends StatelessWidget {
  const _DragCard({
    this.payload,
    this.payloadProvider,
    required this.onTap,
    required this.onDrop,
  });

  final DragPayload? payload;
  final Future<DragPayload?> Function()? payloadProvider;
  final VoidCallback onTap;
  final void Function(bool accepted, String payloadPeek) onDrop;

  @override
  Widget build(BuildContext context) {
    return DragSource(
      payload: payload,
      payloadProvider: payloadProvider,
      onTap: onTap,
      onDropComplete: (accepted) {
        final peek = payload?.data ?? '';
        onDrop(accepted, peek);
      },
      child: Container(
        padding: const EdgeInsets.all(16),
        decoration: BoxDecoration(
          color: Colors.blueGrey.shade900,
          borderRadius: BorderRadius.circular(12),
          border: Border.all(color: Colors.blueGrey.shade700),
        ),
        child: Row(
          children: [
            const Icon(Icons.drag_indicator, size: 32),
            const SizedBox(width: 12),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    payload is TextPayload
                        ? 'Encrypted text payload'
                        : 'Temp file (created on drag)',
                    style: const TextStyle(fontWeight: FontWeight.w600),
                  ),
                  const SizedBox(height: 4),
                  Text(
                    'Drag me',
                    style: TextStyle(color: Colors.grey.shade400, fontSize: 12),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

/// Demonstrates `imageKey`: only the cover image is rasterized as the ghost
/// preview. The "PREMIUM" badge and the file name label below are excluded.
class _CustomGhostCard extends StatefulWidget {
  const _CustomGhostCard({required this.onTap, required this.onDrop});

  final VoidCallback onTap;
  final void Function(bool accepted) onDrop;

  @override
  State<_CustomGhostCard> createState() => _CustomGhostCardState();
}

class _CustomGhostCardState extends State<_CustomGhostCard> {
  final _coverKey = GlobalKey();
  static final _rng = Random();

  /// Random pixel-art cover so the example doesn't need any asset files.
  Widget _buildCover() {
    return Container(
      width: 140,
      height: 140,
      decoration: BoxDecoration(
        gradient: LinearGradient(
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
          colors: [
            Color.fromARGB(255, _rng.nextInt(256), _rng.nextInt(256), 255),
            Color.fromARGB(255, 255, _rng.nextInt(256), _rng.nextInt(256)),
          ],
        ),
        borderRadius: BorderRadius.circular(8),
      ),
      child: const Center(
        child: Icon(Icons.image, size: 48, color: Colors.white70),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return DragSource(
      // Picked solely so the demo runs without external state; in real code
      // this would be the user's actual file path.
      payload: FilePayload('${Directory.systemTemp.path}\\cover_demo.txt'),
      imageKey: _coverKey,
      onTap: widget.onTap,
      onDropComplete: widget.onDrop,
      child: Container(
        padding: const EdgeInsets.all(12),
        decoration: BoxDecoration(
          color: Colors.blueGrey.shade900,
          borderRadius: BorderRadius.circular(12),
          border: Border.all(color: Colors.blueGrey.shade700),
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Stack(
              children: [
                // ← this is what gets rasterized as the ghost image
                RepaintBoundary(
                  key: _coverKey,
                  child: _buildCover(),
                ),
                // ← this badge is NOT in the ghost image
                Positioned(
                  top: 6,
                  right: 6,
                  child: Container(
                    padding: const EdgeInsets.symmetric(
                        horizontal: 6, vertical: 2),
                    decoration: BoxDecoration(
                      color: Colors.amber,
                      borderRadius: BorderRadius.circular(4),
                    ),
                    child: const Text(
                      'PREMIUM',
                      style: TextStyle(
                        color: Colors.black,
                        fontSize: 10,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 8),
            // ← caption is also NOT in the ghost image
            const Text(
              'cover_only.png',
              style: TextStyle(fontSize: 13, color: Colors.grey),
            ),
          ],
        ),
      ),
    );
  }
}
1
likes
160
points
8
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Native Windows OLE drag source for Flutter. Drag files or text with a custom RepaintBoundary ghost image, using pure Dart FFI.

Repository (GitHub)
View/report issues

Topics

#windows #drag-and-drop #ffi #flutter

License

MIT (license)

Dependencies

ffi, flutter, win32

More

Packages that depend on win_drag_source