JSON Canvas Viewer

Componentes Principais

Visão Geral

O sistema é composto por 5 componentes principais que trabalham em conjunto para fornecer a funcionalidade completa do editor visual de JSON.

MyApp

Arquivo: lib/main.dart

Responsabilidade: Ponto de entrada e configuração global da aplicação.

Implementação

class MyApp extends StatelessWidget {
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'JSON Canvas Viewer',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        brightness: Brightness.dark,
        scaffoldBackgroundColor: EditorTheme.backgroundColor,
        colorScheme: const ColorScheme.dark(
          surface: EditorTheme.surfaceColor,
          primary: EditorTheme.textColor,
          onSurface: EditorTheme.textColor,
        ),
      ),
      home: const HomePage(),
    );
  }
}

Características


HomePage

Arquivo: lib/src/modules/home/ui/home_page.dart

Responsabilidade: Coordenador principal, gerencia estado compartilhado e comunicação entre editor e canvas.

Estado Gerenciado

class _HomePageState extends State<HomePage> {
  String _jsonData = '';            // JSON atual
  bool _isValid = false;            // Validação
  bool _isUpdatingFromCanvas = false;  // Flag anti-loop
}

Métodos Principais

onJsonChanged

Recebe atualizações do editor:

void _onJsonChanged(String jsonData) {
  if (_isUpdatingFromCanvas) return;  // Previne loop
  
  Future.microtask(() {
    if (mounted) {
      setState(() {
        _jsonData = jsonData;
      });
    }
  });
}

onValidationChanged

Recebe status de validação:

void _onValidationChanged(bool isValid) {
  Future.microtask(() {
    if (mounted) {
      setState(() {
        _isValid = isValid;
      });
    }
  });
}

Layout

Row(
  children: [
    Expanded(
      flex: 1,
      child: JsonEditorWidget(
        onJsonChanged: _onJsonChanged,
        onValidationChanged: _onValidationChanged,
        externalJsonData: _jsonData.isNotEmpty ? _jsonData : null,
      ),
    ),
    Expanded(
      flex: 1,
      child: CanvasViewerWidget(
        jsonData: _jsonData,
        isValid: _isValid,
        onJsonUpdated: (updatedJson) {
          // Atualização do canvas
        },
      ),
    ),
  ],
)

JsonEditorWidget

Arquivo: lib/src/modules/home/ui/widgets/json_editor_widget.dart

Responsabilidade: Editor de código JSON com validação em tempo real.

Estado Interno

class _JsonEditorWidgetState extends State<JsonEditorWidget> {
  late CodeController _codeController;
  String _errorMessage = '';
  bool _isValid = false;
  bool _isUpdatingFromExternal = false;
  String _lastExternalData = '';
  Timer? _debounceTimer;
}

Debouncing

Implementa timer de 300ms para otimizar performance:

void _onCodeChanged() {
  if (_isUpdatingFromExternal) return;
  
  _debounceTimer?.cancel();
  _debounceTimer = Timer(const Duration(milliseconds: 300), () {
    final text = _codeController.text;
    widget.onJsonChanged(text);
    _validateJson(text);
  });
}

Validação JSON

void _validateJson(String jsonString) {
  try {
    if (jsonString.trim().isEmpty) {
      setState(() {
        _errorMessage = '';
        _isValid = false;
      });
      widget.onValidationChanged(false);
      return;
    }

    const JsonDecoder().convert(jsonString);
    setState(() {
      _errorMessage = '';
      _isValid = true;
    });
    widget.onValidationChanged(true);
  } catch (e) {
    setState(() {
      _errorMessage = 'Invalid JSON: ${e.toString()}';
      _isValid = false;
    });
    widget.onValidationChanged(false);
  }
}

JSON Inicial

Fornece exemplo funcional:

String _getInitialJson() {
  return '''{
  "canvas": {
    "width": 360,
    "height": 640,
    "backgroundColor": "#F8F9FA"
  },
  "elements": [
    {
      "type": "rect",
      "x": "center",
      "y": 100,
      "width": 280,
      "height": 200,
      "color": "#FFFFFF",
      "borderRadius": 20,
      "zIndex": 1
    }
  ]
}''';
}

Interface


CanvasViewerWidget

Arquivo: lib/src/modules/home/ui/widgets/canvas_viewer_widget.dart

Responsabilidade: Visualização do canvas e detecção de estrutura JSON.

Detecção de Canvas

try {
  final decoded = const JsonDecoder().convert(widget.jsonData);
  if (decoded.containsKey('canvas') || decoded.containsKey('elements')) {
    // Usa JsonCanvasViewer
    return JsonCanvasViewer(
      jsonString: widget.jsonData,
      onElementMoved: _updateJsonOnElementMove,
    );
  }
} catch (e) {
  // Fall back para code viewer
}

Atualização de Elementos

Callback chamado quando elemento é movido/redimensionado:

void _updateJsonOnElementMove(
  int elementIndex, 
  double newX, 
  double newY, 
  {double? newWidth, double? newHeight}
) {
  try {
    final decoded = const JsonDecoder().convert(widget.jsonData);
    final elements = decoded['elements'] as List<dynamic>;
    
    if (elementIndex < elements.length) {
      final element = elements[elementIndex] as Map<String, dynamic>;
      element['x'] = newX;
      element['y'] = newY;
      
      if (newWidth != null) element['width'] = newWidth;
      if (newHeight != null) element['height'] = newHeight;
      
      const encoder = JsonEncoder.withIndent('  ');
      final updatedJson = encoder.convert(decoded);
      
      Future.microtask(() {
        widget.onJsonUpdated?.call(updatedJson);
      });
    }
  } catch (e) {
    debugPrint('Error updating JSON: $e');
  }
}

Interface


JsonCanvasViewer

Arquivo: lib/src/shared/jsonWidget/json_canvas_widget.dart

Responsabilidade: Motor de renderização do canvas. Parse JSON, calcula dimensões, renderiza elementos e gerencia interações.

Estado Principal

class _JsonCanvasViewerState extends State<JsonCanvasViewer> {
  final _canvasKey = GlobalKey();
  double canvasWidth = 360;
  double canvasHeight = 640;
  double designWidth = 1080;
  double designHeight = 1920;
  Map<String, dynamic>? parsed;
  List<Map<String, dynamic>> elements = [];
  
  // Drag state
  int? _draggedElementIndex;
  Offset? _dragStartPosition;
  bool _isDragging = false;
  
  // Resize state
  int? _resizingElementIndex;
  Offset? _resizeStartPosition;
  bool _isResizing = false;
  String? _resizeHandle;
}

Parse e Setup

void _parseAndSetupJson() {
  try {
    parsed = parseJson(widget.jsonString);
    
    // Extrai dimensões
    final canvas = parsed!['canvas'] as Map<String, dynamic>?;
    designWidth = (canvas?['exportWidth'] as num?)?.toDouble() ?? 1080;
    designHeight = (canvas?['exportHeight'] as num?)?.toDouble() ?? 1920;
    
    // Calcula tamanho do canvas
    final jsonWidth = (canvas?['width'] as num?)?.toDouble();
    final jsonHeight = (canvas?['height'] as num?)?.toDouble();
    
    if (widget.width != null && widget.height != null) {
      canvasWidth = widget.width!;
      canvasHeight = widget.height!;
    } else if (jsonWidth != null && jsonHeight != null) {
      final scale = min(min(800.0 / jsonWidth, 600.0 / jsonHeight), 1.0);
      canvasWidth = jsonWidth * scale;
      canvasHeight = jsonHeight * scale;
    }
    
    // Ordena por zIndex
    elements = List<Map<String, dynamic>>.from(parsed!['elements']);
    elements.sort((a, b) => (a['zIndex'] ?? 0).compareTo(b['zIndex'] ?? 0));
  } catch (e) {
    debugPrint('Error parsing JSON: $e');
  }
}

Sistema de Escala

double _getElementScaleFactor() {
  return math.min(canvasWidth / designWidth, canvasHeight / designHeight);
}

Parsing de Cores

Color _parseColor(String hexColor) {
  hexColor = hexColor.replaceAll('#', '');
  if (hexColor.length == 6) hexColor = 'FF$hexColor';
  return Color(int.parse('0x$hexColor'));
}

Peso de Fonte

FontWeight _getFontWeight(String? weightName) {
  if (weightName == null) return FontWeight.normal;
  
  const weightMap = {
    'thin': FontWeight.w100,
    'light': FontWeight.w300,
    'normal': FontWeight.w400,
    'medium': FontWeight.w500,
    'semi-bold': FontWeight.w600,
    'bold': FontWeight.w700,
    'extra-bold': FontWeight.w800,
    'black': FontWeight.w900,
  };
  
  return weightMap[weightName.toLowerCase()] ?? FontWeight.normal;
}

Renderização

Widget build(BuildContext context) {
  return Container(
    width: canvasWidth,
    height: canvasHeight,
    decoration: BoxDecoration(
      color: _parseColor(canvasBackground),
      border: Border.all(color: Colors.grey.shade300),
    ),
    child: Stack(
      fit: StackFit.expand,
      children: elements.asMap().entries.map((entry) {
        final index = entry.key;
        final el = entry.value;
        
        // Renderiza baseado no tipo
        Widget elementWidget = _renderElement(el);
        
        // Envolve com gestos
        return _wrapWithDragGesture(elementWidget, index);
      }).toList(),
    ),
  );
}

Lifecycle


ApplyRotation

Arquivo: lib/src/shared/jsonWidget/json_canvas_widget.dart

Responsabilidade: Aplica transformação de rotação a widgets.

Implementação

class ApplyRotation extends StatelessWidget {
  const ApplyRotation({
    super.key, 
    required this.child, 
    this.rotation
  });

  final Widget child;
  final double? rotation;

  @override
  Widget build(BuildContext context) {
    if (rotation == null || rotation == 0) return child;
    return Transform.rotate(
      angle: rotation! * (math.pi / 180),  // Converte graus para radianos
      child: child
    );
  }
}

Uso

ApplyRotation(
  rotation: element['rotation'],
  child: Container(...),
)

Comunicação entre Componentes

graph LR
    HP[HomePage]
    JE[JsonEditor]
    CV[CanvasViewer]
    JCV[JsonCanvasViewer]
    
    JE -->|onJsonChanged| HP
    JE -->|onValidationChanged| HP
    HP -->|jsonData| CV
    HP -->|isValid| CV
    HP -->|externalJsonData| JE
    CV -->|onJsonUpdated| HP
    CV -->|jsonString| JCV
    JCV -->|onElementMoved| CV

Próximos Passos