import 'package:flutter/material.dart'; import 'package:notas/models/note.dart'; // Small presentational widget for a note inside the grid. // Keep this widget lightweight and layout-agnostic: it should not force // width/height constraints (so it works inside different parent layouts // like MasonryGridView or Draggable feedback). Visual styling only. class NoteCard extends StatefulWidget { const NoteCard({ super.key, required this.note, this.onTap, this.isDragging = false, this.borderColor, }); final Note note; final VoidCallback? onTap; final bool isDragging; final Color? borderColor; @override State createState() => _NoteCardState(); } class _NoteCardState extends State { bool _isPressed = false; @override Widget build(BuildContext context) { final bool showGrabbing = widget.isDragging || _isPressed; return MouseRegion( cursor: showGrabbing ? SystemMouseCursors.grabbing : SystemMouseCursors.grab, child: GestureDetector( onTapDown: widget.onTap == null ? null : (_) { setState(() { _isPressed = true; }); }, onTapUp: widget.onTap == null ? null : (_) { setState(() { _isPressed = false; }); }, onTapCancel: widget.onTap == null ? null : () { setState(() { _isPressed = false; }); }, onTap: widget.onTap, child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color.fromRGBO(24, 25, 26, 1), borderRadius: BorderRadius.circular(12), border: Border.all( color: widget.borderColor ?? Colors.white24, width: 1, ), ), child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { // Estimate whether the body will exceed 20 lines without always // running the expensive TextPainter layout. This heuristic counts // newline characters and estimates wrapped lines based on an // average characters-per-line to handle many short lines well. final List rawLines = widget.note.body.split('\n'); const int avgCharsPerLine = 40; int estimatedLines = 0; for (final String line in rawLines) { estimatedLines += (line.trim().length ~/ avgCharsPerLine) + 1; } final bool needsPreciseMeasurement = estimatedLines > 20; final bool isBodyTruncated; if (needsPreciseMeasurement) { final TextPainter textPainter = TextPainter( text: TextSpan( text: widget.note.body, style: const TextStyle(color: Colors.white70, fontSize: 14), ), maxLines: 20, textDirection: TextDirection.ltr, )..layout(maxWidth: constraints.maxWidth); isBodyTruncated = textPainter.didExceedMaxLines; } else { isBodyTruncated = false; } return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( widget.note.title, style: const TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 8), Text( widget.note.body, style: const TextStyle(color: Colors.white70, fontSize: 14), maxLines: 20, overflow: TextOverflow.clip, ), if (isBodyTruncated) ...[ const SizedBox(height: 4), const Text( '...', style: TextStyle( color: Colors.white54, fontSize: 18, height: 1, ), ), ], ], ); }, ), ), ), ); } }