diff --git a/lib/src/add_ons/drawing_tools_ui/drawing_tools_dialog.dart b/lib/src/add_ons/drawing_tools_ui/drawing_tools_dialog.dart index 259282ac4..3980181f6 100644 --- a/lib/src/add_ons/drawing_tools_ui/drawing_tools_dialog.dart +++ b/lib/src/add_ons/drawing_tools_ui/drawing_tools_dialog.dart @@ -53,39 +53,39 @@ class _DrawingToolsDialogState extends State { DropdownButton( value: _selectedDrawingTool, hint: Text(ChartLocalization.of(context).selectDrawingTool), - items: const >[ - DropdownMenuItem( + items: >[ + const DropdownMenuItem( child: Text('Channel'), value: ChannelDrawingToolConfig(), ), - DropdownMenuItem( + const DropdownMenuItem( child: Text('Continuous'), value: ContinuousDrawingToolConfig(), ), DropdownMenuItem( - child: Text('Fib Fan'), + child: const Text('Fib Fan'), value: FibfanDrawingToolConfig(), ), - DropdownMenuItem( + const DropdownMenuItem( child: Text('Horizontal'), value: HorizontalDrawingToolConfig(), ), - DropdownMenuItem( + const DropdownMenuItem( child: Text('Line'), value: LineDrawingToolConfig(), ), - DropdownMenuItem( + const DropdownMenuItem( child: Text('Ray'), value: RayDrawingToolConfig(), ), - DropdownMenuItem( + const DropdownMenuItem( child: Text('Rectangle'), value: RectangleDrawingToolConfig()), - DropdownMenuItem( + const DropdownMenuItem( child: Text('Trend'), value: TrendDrawingToolConfig(), ), - DropdownMenuItem( + const DropdownMenuItem( child: Text('Vertical'), value: VerticalDrawingToolConfig(), ) diff --git a/lib/src/add_ons/drawing_tools_ui/fibfan/fibfan_drawing_tool_config.dart b/lib/src/add_ons/drawing_tools_ui/fibfan/fibfan_drawing_tool_config.dart index 4bf279bc9..924c8c6a8 100644 --- a/lib/src/add_ons/drawing_tools_ui/fibfan/fibfan_drawing_tool_config.dart +++ b/lib/src/add_ons/drawing_tools_ui/fibfan/fibfan_drawing_tool_config.dart @@ -4,6 +4,13 @@ import 'package:deriv_chart/src/add_ons/drawing_tools_ui/callbacks.dart'; import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/drawing_tools/data_model/drawing_pattern.dart'; import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/drawing_tools/data_model/edge_point.dart'; import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/drawing_tools/drawing_data.dart'; +import 'package:deriv_chart/src/deriv_chart/chart/helpers/color_converter.dart'; +import 'package:deriv_chart/src/deriv_chart/chart/helpers/text_style_json_converter.dart'; +import 'package:deriv_chart/src/deriv_chart/interactive_layer/drawing_context.dart'; +import 'package:deriv_chart/src/deriv_chart/interactive_layer/helpers/types.dart'; +import 'package:deriv_chart/src/deriv_chart/interactive_layer/interactable_drawings/fibfan/fibfan_interactable_drawing.dart'; +import 'package:deriv_chart/src/theme/design_tokens/core_design_tokens.dart'; +import 'package:deriv_chart/src/theme/design_tokens/light_theme_design_tokens.dart'; import 'package:deriv_chart/src/theme/painting_styles/line_style.dart'; import 'package:flutter/material.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -13,16 +20,34 @@ part 'fibfan_drawing_tool_config.g.dart'; /// Fibfan drawing tool config @JsonSerializable() +@ColorConverter() class FibfanDrawingToolConfig extends DrawingToolConfig { /// Initializes - const FibfanDrawingToolConfig({ + FibfanDrawingToolConfig({ String? configId, DrawingData? drawingData, List edgePoints = const [], - this.fillStyle = const LineStyle(thickness: 0.9, color: Colors.blue), - this.lineStyle = const LineStyle(thickness: 0.9, color: Colors.white), + this.fibonacciLevelColors = const { + 'level0': CoreDesignTokens.coreColorSolidBlue700, // Blue for 0% + 'level38_2': LightThemeDesignTokens + .semanticColorSeawaterSolidBorderStaticMid, // Cyan for 38.2% + 'level50': LightThemeDesignTokens + .semanticColorMustardSolidBorderStaticHigh, // Amber for 50% + 'level61_8': LightThemeDesignTokens + .semanticColorYellowSolidBorderStaticMid, // Orange for 61.8% + 'level100': CoreDesignTokens.coreColorSolidBlue700, // Blue for 100% + }, + LineStyle? fillStyle, + this.lineStyle = + const LineStyle(color: CoreDesignTokens.coreColorSolidBlue700), + this.labelStyle = const TextStyle( + color: CoreDesignTokens.coreColorSolidBlue700, + fontSize: 12, + ), super.number, - }) : super( + }) : fillStyle = + fillStyle ?? LineStyle(color: fibonacciLevelColors['level0']!), + super( configId: configId, drawingData: drawingData, edgePoints: edgePoints, @@ -45,6 +70,13 @@ class FibfanDrawingToolConfig extends DrawingToolConfig { /// Drawing tool fill style final LineStyle fillStyle; + /// Colors for each Fibonacci level + final Map fibonacciLevelColors; + + /// The style of the label showing on y-axis. + @TextStyleJsonConverter() + final TextStyle labelStyle; + @override DrawingToolItem getItem( UpdateDrawingTool updateDrawingTool, @@ -62,17 +94,39 @@ class FibfanDrawingToolConfig extends DrawingToolConfig { DrawingData? drawingData, LineStyle? lineStyle, LineStyle? fillStyle, + TextStyle? labelStyle, DrawingPatterns? pattern, List? edgePoints, bool? enableLabel, int? number, + Map? fibonacciLevelColors, }) => FibfanDrawingToolConfig( configId: configId ?? this.configId, drawingData: drawingData ?? this.drawingData, lineStyle: lineStyle ?? this.lineStyle, fillStyle: fillStyle ?? this.fillStyle, + labelStyle: labelStyle ?? this.labelStyle, edgePoints: edgePoints ?? this.edgePoints, number: number ?? this.number, + fibonacciLevelColors: fibonacciLevelColors ?? this.fibonacciLevelColors, ); + + @override + FibfanInteractableDrawing getInteractableDrawing( + DrawingContext drawingContext, + GetDrawingState getDrawingState, + ) { + final EdgePoint? startPoint = + edgePoints.isNotEmpty ? edgePoints.first : null; + final EdgePoint? endPoint = edgePoints.length > 1 ? edgePoints[1] : null; + + return FibfanInteractableDrawing( + config: this, + startPoint: startPoint, + endPoint: endPoint, + drawingContext: drawingContext, + getDrawingState: getDrawingState, + ); + } } diff --git a/lib/src/add_ons/drawing_tools_ui/fibfan/fibfan_drawing_tool_config.g.dart b/lib/src/add_ons/drawing_tools_ui/fibfan/fibfan_drawing_tool_config.g.dart index 8f746ff52..92bb91329 100644 --- a/lib/src/add_ons/drawing_tools_ui/fibfan/fibfan_drawing_tool_config.g.dart +++ b/lib/src/add_ons/drawing_tools_ui/fibfan/fibfan_drawing_tool_config.g.dart @@ -17,22 +17,45 @@ FibfanDrawingToolConfig _$FibfanDrawingToolConfigFromJson( ?.map((e) => EdgePoint.fromJson(e as Map)) .toList() ?? const [], + fibonacciLevelColors: + (json['fibonacciLevelColors'] as Map?)?.map( + (k, e) => MapEntry( + k, const ColorConverter().fromJson((e as num).toInt())), + ) ?? + const { + 'level0': CoreDesignTokens.coreColorSolidBlue700, + 'level38_2': LightThemeDesignTokens + .semanticColorSeawaterSolidBorderStaticMid, + 'level50': LightThemeDesignTokens + .semanticColorMustardSolidBorderStaticHigh, + 'level61_8': LightThemeDesignTokens + .semanticColorYellowSolidBorderStaticMid, + 'level100': CoreDesignTokens.coreColorSolidBlue700 + }, fillStyle: json['fillStyle'] == null - ? const LineStyle(thickness: 0.9, color: Colors.blue) + ? null : LineStyle.fromJson(json['fillStyle'] as Map), lineStyle: json['lineStyle'] == null - ? const LineStyle(thickness: 0.9, color: Colors.white) + ? const LineStyle(color: CoreDesignTokens.coreColorSolidBlue700) : LineStyle.fromJson(json['lineStyle'] as Map), + labelStyle: json['labelStyle'] == null + ? const TextStyle( + color: CoreDesignTokens.coreColorSolidBlue700, fontSize: 12) + : const TextStyleJsonConverter() + .fromJson(json['labelStyle'] as Map), number: (json['number'] as num?)?.toInt() ?? 0, ); Map _$FibfanDrawingToolConfigToJson( FibfanDrawingToolConfig instance) => { + 'configId': instance.configId, 'number': instance.number, 'drawingData': instance.drawingData, 'edgePoints': instance.edgePoints, - 'configId': instance.configId, 'lineStyle': instance.lineStyle, 'fillStyle': instance.fillStyle, + 'fibonacciLevelColors': instance.fibonacciLevelColors + .map((k, e) => MapEntry(k, const ColorConverter().toJson(e))), + 'labelStyle': const TextStyleJsonConverter().toJson(instance.labelStyle), }; diff --git a/lib/src/add_ons/drawing_tools_ui/fibfan/fibfan_drawing_tool_item.dart b/lib/src/add_ons/drawing_tools_ui/fibfan/fibfan_drawing_tool_item.dart index a7d26f070..0eb1498fd 100644 --- a/lib/src/add_ons/drawing_tools_ui/fibfan/fibfan_drawing_tool_item.dart +++ b/lib/src/add_ons/drawing_tools_ui/fibfan/fibfan_drawing_tool_item.dart @@ -10,15 +10,15 @@ import '../callbacks.dart'; /// Fibfan drawing tool item in the list of drawing tools class FibfanDrawingToolItem extends DrawingToolItem { /// Initializes - const FibfanDrawingToolItem({ + FibfanDrawingToolItem({ required UpdateDrawingTool updateDrawingTool, required VoidCallback deleteDrawingTool, Key? key, - FibfanDrawingToolConfig config = const FibfanDrawingToolConfig(), + FibfanDrawingToolConfig? config, }) : super( key: key, title: 'Fib fan', - config: config, + config: config ?? FibfanDrawingToolConfig(), updateDrawingTool: updateDrawingTool, deleteDrawingTool: deleteDrawingTool, ); diff --git a/lib/src/deriv_chart/interactive_layer/helpers/paint_helpers.dart b/lib/src/deriv_chart/interactive_layer/helpers/paint_helpers.dart index 12be83211..e599d76d7 100644 --- a/lib/src/deriv_chart/interactive_layer/helpers/paint_helpers.dart +++ b/lib/src/deriv_chart/interactive_layer/helpers/paint_helpers.dart @@ -1,12 +1,20 @@ import 'dart:ui'; +import 'package:deriv_chart/src/add_ons/drawing_tools_ui/drawing_tool_config.dart'; import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/chart_data.dart'; import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/drawing_tools/data_model/drawing_paint_style.dart'; import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/drawing_tools/data_model/edge_point.dart'; +import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/models/animation_info.dart'; import 'package:deriv_chart/src/deriv_chart/chart/helpers/chart_date_utils.dart'; +import 'package:deriv_chart/src/deriv_chart/interactive_layer/interactive_layer_export.dart'; +import 'package:deriv_chart/src/models/chart_config.dart'; +import 'package:deriv_chart/src/theme/chart_theme.dart'; import 'package:deriv_chart/src/theme/painting_styles/line_style.dart'; import 'package:flutter/material.dart'; +import '../enums/drawing_tool_state.dart'; +import 'types.dart'; + /// Draws alignment guides (horizontal and vertical lines) for a single point void drawPointAlignmentGuides(Canvas canvas, Size size, Offset pointOffset, {Color lineColor = const Color(0x80FFFFFF)}) { @@ -202,7 +210,7 @@ void drawValueLabel({ final TextPainter textPainter = _getTextPainter( formattedValue, textStyle: textStyle.copyWith( - color: textStyle.color?.withOpacity(animationProgress), + color: color.withOpacity(animationProgress), ), )..layout(); @@ -283,7 +291,7 @@ void drawEpochLabel({ final TextPainter textPainter = _getTextPainter( formattedTime, textStyle: textStyle.copyWith( - color: textStyle.color?.withOpacity(animationProgress), + color: color.withOpacity(animationProgress), ), )..layout(); @@ -327,6 +335,65 @@ void drawEpochLabel({ ); } +/// Helper method to draw labels with proper z-index based on drag state. +/// +/// This method handles the logic for drawing labels in the correct order +/// to ensure the dragged point's labels appear on top of the non-dragged point's labels. +/// +/// **Parameters:** +/// - [canvas]: The canvas to draw on +/// - [size]: The size of the drawing area +/// - [animationInfo]: Animation information for state changes +/// - [chartConfig]: Chart configuration +/// - [chartTheme]: Chart theme +/// - [getDrawingState]: Function to get the current drawing state +/// - [drawStartPointLabel]: Callback function to draw the start point label +/// - [drawEndPointLabel]: Callback function to draw the end point label +/// - [isDraggingStartPoint]: Whether the start point is currently being dragged +/// - [isDraggingEndPoint]: Whether the end point is currently being dragged +/// +/// **Usage:** +/// This function is designed to be reusable across different drawing tools that have +/// two edge points and need proper z-index handling during drag operations. +void drawLabelsWithZIndex({ + required Canvas canvas, + required Size size, + required AnimationInfo animationInfo, + required ChartConfig chartConfig, + required ChartTheme chartTheme, + required GetDrawingState getDrawingState, + required InteractableDrawing drawing, + required void Function() drawStartPointLabel, + required void Function() drawEndPointLabel, + required bool isDraggingStartPoint, + required bool isDraggingEndPoint, +}) { + if (!getDrawingState(drawing).contains(DrawingToolState.selected)) { + return; + } + + // When dragging individual points, draw the non-dragged point first (lower z-index) + // and the dragged point last (higher z-index) + if (getDrawingState(drawing).contains(DrawingToolState.dragging) && + (isDraggingStartPoint || isDraggingEndPoint)) { + if (isDraggingStartPoint) { + // Start point is being dragged, so draw end point first (lower z-index) + drawEndPointLabel(); + // Then draw start point (higher z-index) + drawStartPointLabel(); + } else { + // End point is being dragged, so draw start point first (lower z-index) + drawStartPointLabel(); + // Then draw end point (higher z-index) + drawEndPointLabel(); + } + } else { + // Default behavior when not dragging individual points + drawStartPointLabel(); + drawEndPointLabel(); + } +} + /// Returns a [TextPainter] for the given formatted value and color. TextPainter _getTextPainter( String formattedValue, { diff --git a/lib/src/deriv_chart/interactive_layer/interactable_drawings/fibfan/drag_state.dart b/lib/src/deriv_chart/interactive_layer/interactable_drawings/fibfan/drag_state.dart new file mode 100644 index 000000000..e5aca6db3 --- /dev/null +++ b/lib/src/deriv_chart/interactive_layer/interactable_drawings/fibfan/drag_state.dart @@ -0,0 +1,25 @@ +/// Enum representing the different drag states for Fibonacci Fan drawing tool. +/// +/// This enum provides clear, explicit states for drag interactions, improving +/// code readability and reducing the risk of misinterpreting the drag state +/// compared to using a nullable boolean. +enum FibfanDragState { + /// User is dragging the start point of the fan. + /// + /// In this state, only the start point moves while the end point remains fixed. + /// This allows users to adjust the origin of the Fibonacci fan lines. + draggingStartPoint, + + /// User is dragging the end point of the fan. + /// + /// In this state, only the end point moves while the start point remains fixed. + /// This allows users to adjust the direction and scale of the Fibonacci fan lines. + draggingEndPoint, + + /// User is dragging the entire fan. + /// + /// In this state, both start and end points move together, maintaining their + /// relative positions. This allows users to reposition the entire fan without + /// changing its shape or orientation. + draggingEntireFan, +} diff --git a/lib/src/deriv_chart/interactive_layer/interactable_drawings/fibfan/fibfan_adding_preview_desktop.dart b/lib/src/deriv_chart/interactive_layer/interactable_drawings/fibfan/fibfan_adding_preview_desktop.dart new file mode 100644 index 000000000..85c6bd8b0 --- /dev/null +++ b/lib/src/deriv_chart/interactive_layer/interactable_drawings/fibfan/fibfan_adding_preview_desktop.dart @@ -0,0 +1,276 @@ +import 'dart:ui'; + +import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/chart_data.dart'; +import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/drawing_tools/data_model/drawing_paint_style.dart'; +import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/drawing_tools/data_model/edge_point.dart'; +import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/models/animation_info.dart'; +import 'package:deriv_chart/src/deriv_chart/interactive_layer/helpers/paint_helpers.dart'; +import 'package:deriv_chart/src/deriv_chart/interactive_layer/interactable_drawings/fibfan/helpers.dart'; +import 'package:deriv_chart/src/models/chart_config.dart'; +import 'package:deriv_chart/src/theme/chart_theme.dart'; +import 'package:deriv_chart/src/theme/painting_styles/line_style.dart'; +import 'package:flutter/gestures.dart'; + +import '../../helpers/types.dart'; +import '../../interactive_layer_states/interactive_adding_tool_state.dart'; +import '../drawing_adding_preview.dart'; +import 'fibfan_interactable_drawing.dart'; + +/// Desktop-optimized preview handler for Fibonacci Fan creation. +/// +/// This class provides a mouse-friendly interface for creating Fibonacci Fan +/// drawings on desktop devices. It implements a two-step creation process +/// that leverages mouse hover and click interactions for precise point placement. +/// +/// **Desktop-Specific Features:** +/// - Two-step creation process (first click for start, second for end) +/// - Real-time hover preview showing fan from start point to cursor +/// - Precise mouse-based point placement +/// - Alignment guides during point placement +/// - Axis labels showing exact coordinate values +/// +/// **Creation Workflow:** +/// 1. User selects Fibonacci Fan tool +/// 2. First click sets the start point +/// 3. Mouse movement shows live preview fan from start to cursor +/// 4. Second click sets the end point and completes the drawing +/// +/// **Visual Feedback:** +/// - Alignment guides appear at hover position and start point +/// - Live preview fan lines follow mouse movement +/// - Coordinate labels on both axes during creation +/// - Smooth transitions between creation states +/// +/// **Precision Features:** +/// - Pixel-perfect point placement with mouse precision +/// - Real-time coordinate validation and feedback +/// - Visual guides for accurate technical analysis placement +class FibfanAddingPreviewDesktop + extends DrawingAddingPreview { + /// Initializes [FibfanAddingPreviewDesktop]. + /// + /// Creates a desktop-optimized preview handler that manages the two-step + /// creation process for Fibonacci Fan drawings. The handler tracks mouse + /// position and manages the creation state transitions. + /// + /// **Parameters:** + /// - [interactiveLayerBehaviour]: Desktop interaction behavior handler + /// - [interactableDrawing]: The Fibonacci Fan drawing being created + /// - [onAddingStateChange]: Callback for adding state changes + FibfanAddingPreviewDesktop({ + required super.interactiveLayerBehaviour, + required super.interactableDrawing, + required super.onAddingStateChange, + }); + + /// Current mouse hover position for real-time preview. + /// + /// Tracks the mouse cursor position to enable live preview functionality. + /// When the start point is set but end point is null, a preview fan is + /// drawn from the start point to this hover position, giving users + /// immediate visual feedback of the final result. + /// + /// **Usage:** + /// - Updated continuously during mouse movement via [onHover] + /// - Used in [paint] method to render preview fan lines + /// - Enables real-time coordinate display on chart axes + /// - Reset when creation process completes + Offset? _hoverPosition; + + @override + bool hitTest(Offset offset, EpochToX epochToX, QuoteToY quoteToY) => false; + + @override + void onHover(PointerHoverEvent event, EpochFromX epochFromX, + QuoteFromY quoteFromY, EpochToX epochToX, QuoteToY quoteToY) { + _hoverPosition = event.localPosition; + } + + @override + String get id => 'Fibfan-adding-preview-desktop'; + + @override + void paint( + Canvas canvas, + Size size, + EpochToX epochToX, + QuoteToY quoteToY, + AnimationInfo animationInfo, + ChartConfig chartConfig, + ChartTheme chartTheme, + GetDrawingState drawingState, + ) { + final LineStyle lineStyle = interactableDrawing.config.lineStyle; + final LineStyle fillStyle = interactableDrawing.config.fillStyle; + final DrawingPaintStyle paintStyle = DrawingPaintStyle(); + + if (interactableDrawing.startPoint == null && _hoverPosition != null) { + final Offset pointOffset = Offset( + _hoverPosition!.dx, + _hoverPosition!.dy, + ); + // Use the same color for edge points (from level0 which matches level100) + final Color edgePointColor = + interactableDrawing.config.fibonacciLevelColors['level0'] ?? + interactableDrawing.config.lineStyle.color; + final LineStyle edgePointLineStyle = + interactableDrawing.config.lineStyle.copyWith(color: edgePointColor); + + drawPointAlignmentGuides(canvas, size, pointOffset, + lineColor: edgePointColor); + + // Draw preview point at hover position + drawPointOffset( + pointOffset, + epochToX, + quoteToY, + canvas, + paintStyle, + edgePointLineStyle, + radius: FibfanConstants.pointRadius, + ); + } + + if (interactableDrawing.startPoint != null && _hoverPosition != null) { + // Draw preview fan from start point to hover position + final Offset startOffset = Offset( + epochToX(interactableDrawing.startPoint!.epoch), + quoteToY(interactableDrawing.startPoint!.quote), + ); + + // Validate coordinates before proceeding + if (!FibonacciFanHelpers.areTwoOffsetsValid( + startOffset, _hoverPosition!)) { + return; + } + + final double deltaX = _hoverPosition!.dx - startOffset.dx; + final double deltaY = _hoverPosition!.dy - startOffset.dy; + + // Only draw if we have meaningful deltas + if (FibonacciFanHelpers.areDeltasMeaningful(deltaX, deltaY)) { + // Draw filled areas between fan lines + FibonacciFanHelpers.drawFanFills( + canvas, startOffset, deltaX, deltaY, size, paintStyle, fillStyle); + // Draw fan lines + FibonacciFanHelpers.drawFanLines( + canvas, startOffset, deltaX, deltaY, size, paintStyle, lineStyle, + fibonacciLevelColors: + interactableDrawing.config.fibonacciLevelColors); + } + // Draw labels for the fan lines + FibonacciFanHelpers.drawFanLabels( + canvas, startOffset, deltaX, deltaY, size, lineStyle, + fibonacciLevelColors: + interactableDrawing.config.fibonacciLevelColors); + + // Use the same color for edge points (from level0 which matches level100) + final Color edgePointColor = + interactableDrawing.config.fibonacciLevelColors['level0'] ?? + interactableDrawing.config.lineStyle.color; + final LineStyle edgePointLineStyle = + interactableDrawing.config.lineStyle.copyWith(color: edgePointColor); + + // Draw the control points + // Draw start point (already placed) + drawPointOffset( + startOffset, + epochToX, + quoteToY, + canvas, + paintStyle, + edgePointLineStyle, + radius: FibfanConstants.pointRadius, + ); + + // Draw end point at hover position (preview) + final Offset endPointOffset = Offset( + _hoverPosition!.dx, + _hoverPosition!.dy, + ); + drawPointOffset( + endPointOffset, + epochToX, + quoteToY, + canvas, + paintStyle, + edgePointLineStyle, + radius: FibfanConstants.pointRadius, + ); + + drawPointAlignmentGuides(canvas, size, endPointOffset, + lineColor: edgePointColor); + } + } + + @override + void paintOverYAxis( + Canvas canvas, + Size size, + EpochToX epochToX, + QuoteToY quoteToY, + EpochFromX? epochFromX, + QuoteFromY? quoteFromY, + AnimationInfo animationInfo, + ChartConfig chartConfig, + ChartTheme chartTheme, + GetDrawingState getDrawingState, + ) { + if (_hoverPosition != null) { + // Use the same color for labels as edge points + final Color edgePointColor = + interactableDrawing.config.fibonacciLevelColors['level0'] ?? + interactableDrawing.config.lineStyle.color; + + drawValueLabel( + canvas: canvas, + quoteToY: quoteToY, + value: interactiveLayerBehaviour.interactiveLayer + .quoteFromY(_hoverPosition!.dy), + pipSize: chartConfig.pipSize, + size: size, + color: edgePointColor, + backgroundColor: chartTheme.backgroundColor, + textStyle: interactableDrawing.config.labelStyle, + ); + drawEpochLabel( + canvas: canvas, + epochToX: epochToX, + epoch: interactiveLayerBehaviour.interactiveLayer + .epochFromX(_hoverPosition!.dx), + size: size, + textStyle: interactableDrawing.config.labelStyle, + animationProgress: animationInfo.stateChangePercent, + color: edgePointColor, + backgroundColor: chartTheme.backgroundColor, + ); + } + } + + @override + void onCreateTap( + TapUpDetails details, + EpochFromX epochFromX, + QuoteFromY quoteFromY, + EpochToX epochToX, + QuoteToY quoteToY, + ) { + if (interactableDrawing.startPoint == null) { + // First tap - set start point + interactableDrawing.startPoint = EdgePoint( + epoch: epochFromX(details.localPosition.dx), + quote: quoteFromY(details.localPosition.dy), + ); + // Notify that we've completed step 1 of 2 + onAddingStateChange(AddingStateInfo(1, 2)); + } else if (interactableDrawing.endPoint == null) { + // Second tap - set end point and complete the drawing + interactableDrawing.endPoint = EdgePoint( + epoch: epochFromX(details.localPosition.dx), + quote: quoteFromY(details.localPosition.dy), + ); + // Notify that we've completed step 2 of 2 (finished) + onAddingStateChange(AddingStateInfo(2, 2)); + } + } +} diff --git a/lib/src/deriv_chart/interactive_layer/interactable_drawings/fibfan/fibfan_adding_preview_mobile.dart b/lib/src/deriv_chart/interactive_layer/interactable_drawings/fibfan/fibfan_adding_preview_mobile.dart new file mode 100644 index 000000000..94af6d049 --- /dev/null +++ b/lib/src/deriv_chart/interactive_layer/interactable_drawings/fibfan/fibfan_adding_preview_mobile.dart @@ -0,0 +1,410 @@ +import 'dart:math' as math; +import 'dart:ui'; + +import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/chart_data.dart'; +import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/drawing_tools/data_model/drawing_paint_style.dart'; +import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/drawing_tools/data_model/edge_point.dart'; +import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/models/animation_info.dart'; +import 'package:deriv_chart/src/deriv_chart/interactive_layer/helpers/paint_helpers.dart'; +import 'package:deriv_chart/src/models/chart_config.dart'; +import 'package:deriv_chart/src/theme/chart_theme.dart'; +import 'package:deriv_chart/src/theme/painting_styles/line_style.dart'; +import 'package:flutter/gestures.dart'; + +import '../../helpers/types.dart'; +import '../../interactive_layer_states/interactive_adding_tool_state.dart'; +import '../fibfan/helpers.dart'; +import '../drawing_adding_preview.dart'; +import 'fibfan_interactable_drawing.dart'; + +/// Mobile-optimized preview handler for Fibonacci Fan creation. +/// +/// This class provides a touch-friendly interface for creating Fibonacci Fan +/// drawings on mobile devices. Unlike desktop behavior that relies on mouse +/// hover and click interactions, mobile behavior pre-positions both points +/// and allows immediate drag-based editing. +/// +/// **Mobile-Specific Features:** +/// - Auto-positioning of start and end points for immediate usability +/// - Touch-optimized drag interactions for point adjustment +/// - Dashed line previews to distinguish from final drawings +/// - Single-tap completion (no multi-step creation process) +/// - Larger touch targets for better mobile interaction +/// +/// **Positioning Strategy:** +/// The mobile implementation automatically places the fan points in optimal +/// positions based on screen dimensions: +/// - Start point: 6% from left edge, vertically centered (50%) +/// - End point: 65% from left edge, upper portion (30%) +/// - This creates an upward-trending fan suitable for most analysis scenarios +/// +/// **User Workflow:** +/// 1. User selects Fibonacci Fan tool +/// 2. Preview appears with pre-positioned points +/// 3. User can drag individual points or entire fan to adjust +/// 4. Single tap completes the drawing +class FibfanAddingPreviewMobile + extends DrawingAddingPreview { + /// Initializes [FibfanAddingPreviewMobile] with auto-positioned points. + /// + /// Creates a mobile-optimized preview that automatically positions the + /// start and end points in sensible locations based on screen dimensions. + /// This eliminates the need for multi-step point placement on touch devices. + /// + /// **Auto-Positioning Logic:** + /// - Calculates optimal positions using screen dimension ratios + /// - Places start point in left-center area for trend origin + /// - Places end point in upper-right area for upward trend + /// - Provides fallback coordinates if screen dimensions unavailable + /// + /// **Parameters:** + /// - [interactiveLayerBehaviour]: Mobile interaction behavior handler + /// - [interactableDrawing]: The Fibonacci Fan drawing being created + /// - [onAddingStateChange]: Callback for adding state changes + FibfanAddingPreviewMobile({ + required super.interactiveLayerBehaviour, + required super.interactableDrawing, + required super.onAddingStateChange, + }) { + if (interactableDrawing.startPoint == null) { + final interactiveLayer = interactiveLayerBehaviour.interactiveLayer; + final Size? layerSize = interactiveLayer.drawingContext.fullSize; + + if (layerSize != null) { + // Position start point around the chart data area (middle-right region) + final double startX = + layerSize.width * FibfanConstants.mobileStartXRatio; + final double startY = + layerSize.height * FibfanConstants.mobileStartYRatio; + + interactableDrawing.startPoint = EdgePoint( + epoch: interactiveLayer.epochFromX(startX), + quote: interactiveLayer.quoteFromY(startY), + ); + } else { + // Fallback to center if size is not available + interactableDrawing.startPoint = EdgePoint( + epoch: interactiveLayer.epochFromX(0), + quote: interactiveLayer.quoteFromY(0), + ); + } + } + + if (interactableDrawing.endPoint == null) { + final interactiveLayer = interactiveLayerBehaviour.interactiveLayer; + final Size? layerSize = interactiveLayer.drawingContext.fullSize; + + if (layerSize != null) { + // Position end point to the right and above start point + // This creates a proper upward-oriented Fibonacci fan + final double endX = layerSize.width * FibfanConstants.mobileEndXRatio; + final double endY = layerSize.height * + FibfanConstants.mobileEndYRatio; // Above start point (0.5) + + interactableDrawing.endPoint = EdgePoint( + epoch: interactiveLayer.epochFromX(endX), + quote: interactiveLayer.quoteFromY(endY), + ); + } else { + // Fallback with proper orientation if size is not available + const double fallbackX = FibfanConstants.mobileFallbackX; + const double fallbackY = + FibfanConstants.mobileFallbackY; // Above start point + + interactableDrawing.endPoint = EdgePoint( + epoch: interactiveLayer.epochFromX(fallbackX), + quote: interactiveLayer.quoteFromY(fallbackY), + ); + } + } + } + + /// Track if the drawing is currently being dragged + bool _isDragging = false; + + @override + bool hitTest(Offset offset, EpochToX epochToX, QuoteToY quoteToY) { + return interactableDrawing.hitTest(offset, epochToX, quoteToY); + } + + @override + String get id => 'Fibfan-adding-preview-mobile'; + + @override + void onDragStart(DragStartDetails details, EpochFromX epochFromX, + QuoteFromY quoteFromY, EpochToX epochToX, QuoteToY quoteToY) { + _isDragging = true; + interactableDrawing.onDragStart( + details, epochFromX, quoteFromY, epochToX, quoteToY); + } + + @override + void onDragUpdate(DragUpdateDetails details, EpochFromX epochFromX, + QuoteFromY quoteFromY, EpochToX epochToX, QuoteToY quoteToY) { + interactableDrawing.onDragUpdate( + details, + epochFromX, + quoteFromY, + epochToX, + quoteToY, + ); + } + + /// Handle drag end to reset drag state + @override + void onDragEnd(DragEndDetails details, EpochFromX epochFromX, + QuoteFromY quoteFromY, EpochToX epochToX, QuoteToY quoteToY) { + _isDragging = false; + // Call parent implementation if it exists + super.onDragEnd(details, epochFromX, quoteFromY, epochToX, quoteToY); + } + + @override + void paint( + Canvas canvas, + Size size, + EpochToX epochToX, + QuoteToY quoteToY, + AnimationInfo animationInfo, + ChartConfig chartConfig, + ChartTheme chartTheme, + GetDrawingState drawingState, + ) { + if (interactableDrawing.startPoint != null && + interactableDrawing.endPoint != null) { + final Offset startOffset = Offset( + epochToX(interactableDrawing.startPoint!.epoch), + quoteToY(interactableDrawing.startPoint!.quote), + ); + final Offset endOffset = Offset( + epochToX(interactableDrawing.endPoint!.epoch), + quoteToY(interactableDrawing.endPoint!.quote), + ); + + // Validate coordinates before proceeding + if (!FibonacciFanHelpers.areTwoOffsetsValid(startOffset, endOffset)) { + return; + } + + // Calculate the base vector + final double deltaX = endOffset.dx - startOffset.dx; + final double deltaY = endOffset.dy - startOffset.dy; + + // Only draw if we have meaningful deltas + if (FibonacciFanHelpers.areDeltasMeaningful(deltaX, deltaY)) { + // Draw preview fan lines with dashed style + _drawPreviewFanLines(canvas, startOffset, deltaX, deltaY, size); + } + + // Use the same color for edge points (from level0 which matches level100) + final Color edgePointColor = + interactableDrawing.config.fibonacciLevelColors['level0'] ?? + interactableDrawing.config.lineStyle.color; + final LineStyle edgePointLineStyle = + interactableDrawing.config.lineStyle.copyWith(color: edgePointColor); + + // Draw edge points for the preview + final DrawingPaintStyle paintStyle = DrawingPaintStyle(); + drawPointOffset( + startOffset, + epochToX, + quoteToY, + canvas, + paintStyle, + edgePointLineStyle, + radius: FibfanConstants.pointRadius, + ); + drawPointOffset( + endOffset, + epochToX, + quoteToY, + canvas, + paintStyle, + edgePointLineStyle, + radius: FibfanConstants.pointRadius, + ); + + // Draw alignment guides on each edge point when dragging + if (_isDragging) { + // Use the same color for alignment guides as edge points + final Color edgePointColor = + interactableDrawing.config.fibonacciLevelColors['level0'] ?? + interactableDrawing.config.lineStyle.color; + + drawPointAlignmentGuides( + canvas, + size, + startOffset, + lineColor: edgePointColor, + ); + drawPointAlignmentGuides( + canvas, + size, + endOffset, + lineColor: edgePointColor, + ); + } + } + } + + /// Draws preview fan lines with dashed style using angle-based calculations + void _drawPreviewFanLines( + Canvas canvas, + Offset startOffset, + double deltaX, + double deltaY, + Size size, + ) { + final Paint dashPaint = FibonacciFanHelpers.getCachedDashPaint( + interactableDrawing.config.lineStyle.color, + interactableDrawing.config.lineStyle.thickness, + FibfanConstants.dashOpacity, + ); + + // Calculate the base angle from start to end point (same as main fan) + final double baseAngle = math.atan2(deltaY, deltaX); + + for (final FibonacciLevel level in FibonacciFanHelpers.fibonacciLevels) { + // Calculate angle: 0% should point to end point, 100% should be horizontal (0 degrees) + // Interpolate between the end angle (baseAngle) and horizontal reference (0 degrees) + const double horizontalAngle = 0; // Horizontal reference + final double fanAngle = + baseAngle + (horizontalAngle - baseAngle) * level.ratio; + + // Extend line to the edge of the screen using angle-based calculations + final double screenWidth = size.width; + final double distanceToEdge = screenWidth - startOffset.dx; + + // Calculate extended point using trigonometry (same as main fan) + final Offset extendedPoint = Offset( + screenWidth, + startOffset.dy + distanceToEdge * math.tan(fanAngle), + ); + + // Validate coordinates before drawing + if (FibonacciFanHelpers.areTwoOffsetsValid(startOffset, extendedPoint)) { + // Draw dashed line + _drawDashedLine(canvas, startOffset, extendedPoint, dashPaint); + } + } + } + + /// Draws a dashed line between two points + void _drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint) { + const double dashWidth = FibfanConstants.dashWidth; + const double dashSpace = FibfanConstants.dashSpace; + final double distance = (end - start).distance; + + // Handle edge cases + if (distance <= 0 || !FibonacciFanHelpers.areTwoOffsetsValid(start, end)) { + return; + } + + final Offset direction = (end - start) / distance; + + double currentDistance = 0; + bool isDash = true; + + while (currentDistance < distance) { + final double segmentLength = isDash ? dashWidth : dashSpace; + final double remainingDistance = distance - currentDistance; + final double actualSegmentLength = + segmentLength > remainingDistance ? remainingDistance : segmentLength; + + if (isDash && actualSegmentLength > 0) { + final Offset segmentStart = start + direction * currentDistance; + final Offset segmentEnd = + start + direction * (currentDistance + actualSegmentLength); + + // Validate segment points before drawing + if (FibonacciFanHelpers.areTwoOffsetsValid(segmentStart, segmentEnd)) { + canvas.drawLine(segmentStart, segmentEnd, paint); + } + } + + currentDistance += actualSegmentLength.toDouble(); + isDash = !isDash; + } + } + + @override + void paintOverYAxis( + Canvas canvas, + Size size, + EpochToX epochToX, + QuoteToY quoteToY, + EpochFromX? epochFromX, + QuoteFromY? quoteFromY, + AnimationInfo animationInfo, + ChartConfig chartConfig, + ChartTheme chartTheme, + GetDrawingState getDrawingState, + ) { + // Draw labels for both edge points when dragging + if (_isDragging && + interactableDrawing.startPoint != null && + interactableDrawing.endPoint != null) { + // Use the same color for labels as edge points + final Color edgePointColor = + interactableDrawing.config.fibonacciLevelColors['level0'] ?? + interactableDrawing.config.lineStyle.color; + + // Draw labels for start point + drawValueLabel( + canvas: canvas, + quoteToY: quoteToY, + value: interactableDrawing.startPoint!.quote, + pipSize: chartConfig.pipSize, + size: size, + color: edgePointColor, + backgroundColor: chartTheme.backgroundColor, + textStyle: interactableDrawing.config.labelStyle, + ); + drawEpochLabel( + canvas: canvas, + epochToX: epochToX, + epoch: interactableDrawing.startPoint!.epoch, + size: size, + textStyle: interactableDrawing.config.labelStyle, + animationProgress: animationInfo.stateChangePercent, + color: edgePointColor, + backgroundColor: chartTheme.backgroundColor, + ); + + // Draw labels for end point + drawValueLabel( + canvas: canvas, + quoteToY: quoteToY, + value: interactableDrawing.endPoint!.quote, + pipSize: chartConfig.pipSize, + size: size, + color: edgePointColor, + backgroundColor: chartTheme.backgroundColor, + textStyle: interactableDrawing.config.labelStyle, + ); + drawEpochLabel( + canvas: canvas, + epochToX: epochToX, + epoch: interactableDrawing.endPoint!.epoch, + size: size, + textStyle: interactableDrawing.config.labelStyle, + animationProgress: animationInfo.stateChangePercent, + color: edgePointColor, + backgroundColor: chartTheme.backgroundColor, + ); + } + } + + @override + void onCreateTap( + TapUpDetails details, + EpochFromX epochFromX, + QuoteFromY quoteFromY, + EpochToX epochToX, + QuoteToY quoteToY, + ) { + // For mobile, we complete the drawing on first tap since we already have both points + // Notify that we've completed step 1 of 1 (finished) + onAddingStateChange(AddingStateInfo(1, 1)); + } +} diff --git a/lib/src/deriv_chart/interactive_layer/interactable_drawings/fibfan/fibfan_interactable_drawing.dart b/lib/src/deriv_chart/interactive_layer/interactable_drawings/fibfan/fibfan_interactable_drawing.dart new file mode 100644 index 000000000..9bf8275f3 --- /dev/null +++ b/lib/src/deriv_chart/interactive_layer/interactable_drawings/fibfan/fibfan_interactable_drawing.dart @@ -0,0 +1,907 @@ +import 'dart:math' as math; +import 'dart:ui' as ui; +import 'package:deriv_chart/src/add_ons/drawing_tools_ui/callbacks.dart'; +import 'package:deriv_chart/src/add_ons/drawing_tools_ui/drawing_tool_config.dart'; +import 'package:deriv_chart/src/add_ons/drawing_tools_ui/fibfan/fibfan_drawing_tool_config.dart'; +import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/chart_data.dart'; +import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/drawing_tools/data_model/drawing_paint_style.dart'; +import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/drawing_tools/data_model/edge_point.dart'; +import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/extensions/extensions.dart'; +import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/models/animation_info.dart'; +import 'package:deriv_chart/src/deriv_chart/interactive_layer/interactable_drawings/drawing_adding_preview.dart'; +import 'package:deriv_chart/src/deriv_chart/interactive_layer/interactable_drawings/fibfan/drag_state.dart'; +import 'package:deriv_chart/src/deriv_chart/interactive_layer/interactable_drawings/fibfan/helpers.dart'; +import 'package:deriv_chart/src/models/axis_range.dart'; +import 'package:deriv_chart/src/models/chart_config.dart'; +import 'package:deriv_chart/src/theme/chart_theme.dart'; +import 'package:deriv_chart/src/theme/painting_styles/line_style.dart'; +import 'package:deriv_chart/src/widgets/color_picker/color_picker_dropdown_button.dart'; +import 'package:deriv_chart/src/widgets/dropdown/line_thickness/line_thickness_dropdown_button.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +import '../../enums/drawing_tool_state.dart'; +import '../../helpers/paint_helpers.dart'; +import '../../helpers/types.dart'; +import '../../interactive_layer_behaviours/interactive_layer_desktop_behaviour.dart'; +import '../../interactive_layer_behaviours/interactive_layer_mobile_behaviour.dart'; +import '../../interactive_layer_states/interactive_adding_tool_state.dart'; +import '../drawing_v2.dart'; +import '../interactable_drawing.dart'; +import 'fibfan_adding_preview_desktop.dart'; +import 'fibfan_adding_preview_mobile.dart'; + +/// Interactable drawing for Fibonacci Fan drawing tool. +/// +/// This class implements a complete Fibonacci Fan technical analysis tool that allows +/// users to draw, interact with, and customize fan lines based on Fibonacci ratios. +/// The fan consists of multiple trend lines emanating from a start point, each +/// representing different Fibonacci retracement levels (0%, 38.2%, 50%, 61.8%, 100%). +/// +/// **Key Features:** +/// - Interactive creation with two-point definition (start and end points) +/// - Real-time preview during creation and editing +/// - Drag support for individual points or entire fan +/// - Hit testing for precise user interaction +/// - Customizable colors and styling +/// - Mobile and desktop optimized behaviors +/// - Automatic label display with percentage values +/// - Fill areas between fan lines for visual clarity +/// +/// **Usage in Technical Analysis:** +/// Fibonacci fans help traders identify potential support and resistance levels +/// by projecting Fibonacci ratios from a significant price movement. The fan +/// lines act as dynamic trend lines that can guide trading decisions. +/// +/// **Interaction States:** +/// - **Creating**: User is placing the start and end points +/// - **Selected**: Fan is selected and shows all visual elements +/// - **Dragging**: User is moving points or the entire fan +/// - **Hovered**: Mouse is over the fan (desktop only) +class FibfanInteractableDrawing + extends InteractableDrawing { + /// Initializes [FibfanInteractableDrawing]. + /// + /// Creates a new Fibonacci Fan drawing with the specified configuration + /// and initial points. The drawing can be created with null points for + /// interactive creation or with predefined points for loading saved drawings. + /// + /// **Parameters:** + /// - [config]: Drawing configuration including colors, styles, and Fibonacci levels + /// - [startPoint]: Initial start point of the fan (can be null for interactive creation) + /// - [endPoint]: Initial end point of the fan (can be null for interactive creation) + /// - [drawingContext]: Context providing canvas dimensions and coordinate conversion + /// - [getDrawingState]: Function to retrieve current drawing state (selected, dragging, etc.) + FibfanInteractableDrawing({ + required FibfanDrawingToolConfig config, + required this.startPoint, + required this.endPoint, + required super.drawingContext, + required super.getDrawingState, + }) : super(drawingConfig: config); + + /// Start point of the fan in epoch/quote coordinates. + /// + /// This point serves as the origin for all fan lines. In technical analysis, + /// this is typically placed at a significant price level (support, resistance, + /// or pivot point) from which Fibonacci projections are calculated. + EdgePoint? startPoint; + + /// End point of the fan in epoch/quote coordinates. + /// + /// This point defines the direction and scale of the fan. The vector from + /// start to end point determines the base angle and magnitude for calculating + /// all Fibonacci fan lines. Each fan line uses this vector multiplied by + /// its respective Fibonacci ratio. + EdgePoint? endPoint; + + /// Tracks the current drag state during user interaction. + /// + /// This state variable enables precise drag behavior by distinguishing between: + /// - `null`: No dragging is currently active + /// - `FibfanDragState.draggingStartPoint`: User is dragging only the start point + /// - `FibfanDragState.draggingEndPoint`: User is dragging only the end point + /// - `FibfanDragState.draggingEntireFan`: User is dragging the entire fan (both points move together) + /// + /// The value is set during [onDragStart] based on hit testing and cleared + /// during [onDragEnd] to reset the interaction state. + FibfanDragState? _dragState; + + /// Current hover position for desktop interactions. + /// + /// Stores the mouse position during hover events to enable real-time + /// preview functionality. This is used primarily during the creation + /// process to show a preview fan from the start point to the cursor. + /// + /// **Note:** This is only used on desktop platforms where hover events + /// are available. Mobile platforms use touch-based interactions instead. + Offset? _hoverPosition; + + @override + void onHover(PointerHoverEvent event, EpochFromX epochFromX, + QuoteFromY quoteFromY, EpochToX epochToX, QuoteToY quoteToY) { + _hoverPosition = event.localPosition; + } + + @override + void onDragStart( + DragStartDetails details, + EpochFromX epochFromX, + QuoteFromY quoteFromY, + EpochToX epochToX, + QuoteToY quoteToY, + ) { + if (startPoint == null || endPoint == null) { + return; + } + + final Offset startOffset = Offset( + epochToX(startPoint!.epoch), + quoteToY(startPoint!.quote), + ); + + final Offset endOffset = Offset( + epochToX(endPoint!.epoch), + quoteToY(endPoint!.quote), + ); + + // Check if the drag is starting on one of the endpoints + final double startDistance = (details.localPosition - startOffset).distance; + final double endDistance = (details.localPosition - endOffset).distance; + + // If the drag is starting on the start point + if (startDistance <= hitTestMargin) { + _dragState = FibfanDragState.draggingStartPoint; + return; + } + + // If the drag is starting on the end point + if (endDistance <= hitTestMargin) { + _dragState = FibfanDragState.draggingEndPoint; + return; + } + + // Check if the drag is on any of the fan lines + if (_hitTestFanLines(details.localPosition, epochToX, quoteToY)) { + _dragState = FibfanDragState.draggingEntireFan; + return; + } + + // Check if the drag is anywhere within the fan area + if (_hitTestFanArea(details.localPosition, epochToX, quoteToY)) { + _dragState = FibfanDragState.draggingEntireFan; + return; + } + } + + @override + bool hitTest(Offset offset, EpochToX epochToX, QuoteToY quoteToY) { + if (startPoint == null || endPoint == null) { + return false; + } + + final isNotSelected = !state.contains(DrawingToolState.selected); + final isOutsideContent = offset.dx > drawingContext.contentSize.width; + + if (isNotSelected && isOutsideContent) { + return false; + } + + // Convert start and end points from epoch/quote to screen coordinates + final Offset startOffset = Offset( + epochToX(startPoint!.epoch), + quoteToY(startPoint!.quote), + ); + final Offset endOffset = Offset( + epochToX(endPoint!.epoch), + quoteToY(endPoint!.quote), + ); + + // Check if the pointer is near either endpoint + final double startDistance = (offset - startOffset).distance; + final double endDistance = (offset - endOffset).distance; + + if (startDistance <= hitTestMargin || endDistance <= hitTestMargin) { + return true; + } + + // Check if the pointer is near any of the fan lines + if (_hitTestFanLines(offset, epochToX, quoteToY)) { + return true; + } + + // Check if the pointer is within the fan area (between the fan lines) + return _hitTestFanArea(offset, epochToX, quoteToY); + } + + /// Helper method to test if a point hits any of the fan lines using angle-based calculations + bool _hitTestFanLines(Offset offset, EpochToX epochToX, QuoteToY quoteToY) { + if (startPoint == null || endPoint == null) { + return false; + } + + final Offset startOffset = Offset( + epochToX(startPoint!.epoch), + quoteToY(startPoint!.quote), + ); + final Offset endOffset = Offset( + epochToX(endPoint!.epoch), + quoteToY(endPoint!.quote), + ); + + // Calculate the base vector and angle + final double deltaX = endOffset.dx - startOffset.dx; + final double deltaY = endOffset.dy - startOffset.dy; + final double baseAngle = math.atan2(deltaY, deltaX); + + // Check each fan line using angle-based calculations + for (final FibonacciLevel level in FibonacciFanHelpers.fibonacciLevels) { + // Calculate angle: 0% should point to end point, 100% should be horizontal (0 degrees) + // Interpolate between the end angle (baseAngle) and horizontal reference (0 degrees) + const double horizontalAngle = 0; // Horizontal reference + final double fanAngle = + baseAngle + (horizontalAngle - baseAngle) * level.ratio; + + // Extend the line to the edge of the screen using trigonometry + final double screenWidth = drawingContext.contentSize.width; + final double distanceToEdge = screenWidth - startOffset.dx; + + final Offset extendedEndPoint = Offset( + screenWidth, + startOffset.dy + distanceToEdge * math.tan(fanAngle), + ); + + // Calculate perpendicular distance from point to line + final double lineLength = (extendedEndPoint - startOffset).distance; + if (lineLength < FibfanConstants.minLineLength) { + continue; + } + + final double distance = + ((extendedEndPoint.dy - startOffset.dy) * offset.dx - + (extendedEndPoint.dx - startOffset.dx) * offset.dy + + extendedEndPoint.dx * startOffset.dy - + extendedEndPoint.dy * startOffset.dx) + .abs() / + lineLength; + + // Check if point is within the line segment + final double dotProduct = (offset.dx - startOffset.dx) * + (extendedEndPoint.dx - startOffset.dx) + + (offset.dy - startOffset.dy) * (extendedEndPoint.dy - startOffset.dy); + + final bool isWithinRange = + dotProduct >= 0 && dotProduct <= lineLength * lineLength; + + if (isWithinRange && distance <= hitTestMargin) { + return true; + } + } + + return false; + } + + /// Helper method to test if a point is within the fan area (between the fan lines) + /// Uses the same logic as drawFanFills to ensure perfect coverage + bool _hitTestFanArea(Offset offset, EpochToX epochToX, QuoteToY quoteToY) { + if (startPoint == null || endPoint == null) { + return false; + } + + final Offset startOffset = Offset( + epochToX(startPoint!.epoch), + quoteToY(startPoint!.quote), + ); + final Offset endOffset = Offset( + epochToX(endPoint!.epoch), + quoteToY(endPoint!.quote), + ); + + final double deltaX = endOffset.dx - startOffset.dx; + final double deltaY = endOffset.dy - startOffset.dy; + + // Use shared calculation method for perfect consistency with drawFanFills + final List> fanPolygons = + FibonacciFanHelpers.calculateFanAreaPolygons( + startOffset: startOffset, + deltaX: deltaX, + deltaY: deltaY, + size: drawingContext.contentSize, + ); + + // Test if point is in any of the calculated polygons + for (final List polygon in fanPolygons) { + if (_isPointInTriangle(offset, polygon[0], polygon[1], polygon[2])) { + return true; + } + } + + return false; + } + + /// Helper method to test if a point is inside a triangle using barycentric coordinates + bool _isPointInTriangle(Offset point, Offset a, Offset b, Offset c) { + // Calculate vectors + final double v0x = c.dx - a.dx; + final double v0y = c.dy - a.dy; + final double v1x = b.dx - a.dx; + final double v1y = b.dy - a.dy; + final double v2x = point.dx - a.dx; + final double v2y = point.dy - a.dy; + + // Calculate dot products + final double dot00 = v0x * v0x + v0y * v0y; + final double dot01 = v0x * v1x + v0y * v1y; + final double dot02 = v0x * v2x + v0y * v2y; + final double dot11 = v1x * v1x + v1y * v1y; + final double dot12 = v1x * v2x + v1y * v2y; + + // Calculate barycentric coordinates + final double invDenom = 1 / (dot00 * dot11 - dot01 * dot01); + final double u = (dot11 * dot02 - dot01 * dot12) * invDenom; + final double v = (dot00 * dot12 - dot01 * dot02) * invDenom; + + // Check if point is in triangle + return (u >= 0) && (v >= 0) && (u + v <= 1); + } + + @override + void paint( + Canvas canvas, + Size size, + EpochToX epochToX, + QuoteToY quoteToY, + AnimationInfo animationInfo, + ChartConfig chartConfig, + ChartTheme chartTheme, + GetDrawingState getDrawingState, + ) { + final LineStyle lineStyle = config.lineStyle; + final LineStyle fillStyle = config.fillStyle; + final DrawingPaintStyle paintStyle = DrawingPaintStyle(); + final drawingState = getDrawingState(this); + + // Handle configuration changes for automatic cache management + final int configHash = _calculateConfigHash(); + FibonacciFanHelpers.handleConfigurationChange(configHash); + + if (startPoint != null && endPoint != null) { + final Offset startOffset = Offset( + epochToX(startPoint!.epoch), + quoteToY(startPoint!.quote), + ); + final Offset endOffset = Offset( + epochToX(endPoint!.epoch), + quoteToY(endPoint!.quote), + ); + + // Calculate the base vector + final double deltaX = endOffset.dx - startOffset.dx; + final double deltaY = endOffset.dy - startOffset.dy; + + // Draw fan lines + FibonacciFanHelpers.drawFanLines( + canvas, startOffset, deltaX, deltaY, size, paintStyle, lineStyle, + fibonacciLevelColors: config.fibonacciLevelColors); + + // Draw labels + if (drawingState.contains(DrawingToolState.selected)) { + // Draw filled areas between fan lines + FibonacciFanHelpers.drawFanFills( + canvas, startOffset, deltaX, deltaY, size, paintStyle, fillStyle, + fibonacciLevelColors: config.fibonacciLevelColors); + FibonacciFanHelpers.drawFanLabels( + canvas, startOffset, deltaX, deltaY, size, lineStyle, + fibonacciLevelColors: config.fibonacciLevelColors); + } + + // Draw endpoints with appropriate visual feedback based on interaction state + if (drawingState.contains(DrawingToolState.selected) || + drawingState.contains(DrawingToolState.dragging)) { + // Use the same color for both edge points (from level0 which matches level100) + final Color edgePointColor = + config.fibonacciLevelColors['level0'] ?? config.lineStyle.color; + final LineStyle edgePointLineStyle = + config.lineStyle.copyWith(color: edgePointColor); + + // Handle individual point dragging with differentiated visual feedback + if (drawingState.contains(DrawingToolState.dragging) && + (_dragState == FibfanDragState.draggingStartPoint || + _dragState == FibfanDragState.draggingEndPoint)) { + // Draw focused circle (glowing effect) only on the point being dragged + // This provides clear visual feedback about which point is actively being manipulated + drawFocusedCircle( + paintStyle, + edgePointLineStyle, + canvas, + _dragState == FibfanDragState.draggingStartPoint + ? startOffset + : endOffset, + FibfanConstants.focusedCircleRadius * + animationInfo.stateChangePercent, + FibfanConstants.focusedCircleStroke * + animationInfo.stateChangePercent, + ); + + // Draw regular point (non-glowing) on the point that is NOT being dragged + // This maintains visibility of the stationary point while clearly distinguishing + // it from the actively dragged point + drawPoint( + _dragState == FibfanDragState.draggingStartPoint + ? endPoint! + : startPoint!, + epochToX, + quoteToY, + canvas, + paintStyle, + edgePointLineStyle, + radius: FibfanConstants.pointRadius, + ); + } else { + // When not dragging individual points (selected state or dragging entire fan), + // show focused circles on both points for general selection feedback + drawPointsFocusedCircle( + paintStyle, + edgePointLineStyle, + canvas, + startOffset, + FibfanConstants.focusedCircleRadius * + animationInfo.stateChangePercent, + FibfanConstants.focusedCircleStroke * + animationInfo.stateChangePercent, + endOffset, + ); + } + } else if (drawingState.contains(DrawingToolState.hovered)) { + // Use the same color for both edge points (from level0 which matches level100) + final Color edgePointColor = + config.fibonacciLevelColors['level0'] ?? config.lineStyle.color; + final LineStyle edgePointLineStyle = + config.lineStyle.copyWith(color: edgePointColor); + + drawPointsFocusedCircle( + paintStyle, + edgePointLineStyle, + canvas, + startOffset, + FibfanConstants.focusedCircleRadius, + FibfanConstants.focusedCircleStroke, + endOffset); + } + + // Draw alignment guides when dragging + if (drawingState.contains(DrawingToolState.dragging)) { + // Use the same color for alignment guides as edge points + final Color edgePointColor = + config.fibonacciLevelColors['level0'] ?? config.lineStyle.color; + + switch (_dragState) { + case FibfanDragState.draggingStartPoint: + drawPointAlignmentGuides(canvas, size, startOffset, + lineColor: edgePointColor); + break; + case FibfanDragState.draggingEndPoint: + drawPointAlignmentGuides(canvas, size, endOffset, + lineColor: edgePointColor); + break; + case FibfanDragState.draggingEntireFan: + drawPointAlignmentGuides(canvas, size, startOffset, + lineColor: edgePointColor); + drawPointAlignmentGuides(canvas, size, endOffset, + lineColor: edgePointColor); + break; + case null: + // No specific drag state, don't draw alignment guides + break; + } + } + } else if (startPoint != null && _hoverPosition != null) { + // Preview mode - draw fan from start point to hover position + final Offset startOffset = Offset( + epochToX(startPoint!.epoch), + quoteToY(startPoint!.quote), + ); + + final double deltaX = _hoverPosition!.dx - startOffset.dx; + final double deltaY = _hoverPosition!.dy - startOffset.dy; + + FibonacciFanHelpers.drawFanLines( + canvas, startOffset, deltaX, deltaY, size, paintStyle, lineStyle); + + // Draw the control points during preview + // Draw start point (already placed) + drawPoint( + startPoint!, + epochToX, + quoteToY, + canvas, + paintStyle, + config.lineStyle, + radius: FibfanConstants.pointRadius, + ); + + // Draw preview end point at hover position + final Offset hoverPointOffset = Offset( + _hoverPosition!.dx, + _hoverPosition!.dy, + ); + drawPointOffset( + hoverPointOffset, + epochToX, + quoteToY, + canvas, + paintStyle, + config.lineStyle, + radius: FibfanConstants.pointRadius, + ); + + // Use the same color for alignment guides as edge points + final Color edgePointColor = + config.fibonacciLevelColors['level0'] ?? config.lineStyle.color; + drawPointAlignmentGuides(canvas, size, startOffset, + lineColor: edgePointColor); + } + } + + @override + void paintOverYAxis( + ui.Canvas canvas, + ui.Size size, + EpochToX epochToX, + QuoteToY quoteToY, + int Function(double)? epochFromX, + double Function(double)? quoteFromY, + AnimationInfo animationInfo, + ChartConfig chartConfig, + ChartTheme chartTheme, + GetDrawingState getDrawingState, + ) { + drawLabelsWithZIndex( + canvas: canvas, + size: size, + animationInfo: animationInfo, + chartConfig: chartConfig, + chartTheme: chartTheme, + getDrawingState: getDrawingState, + drawing: this, + isDraggingStartPoint: _dragState == FibfanDragState.draggingStartPoint, + isDraggingEndPoint: _dragState == FibfanDragState.draggingEndPoint, + drawStartPointLabel: () { + if (startPoint != null) { + // Use the same color for labels as edge points + final Color edgePointColor = + config.fibonacciLevelColors['level0'] ?? config.lineStyle.color; + drawValueLabel( + canvas: canvas, + quoteToY: quoteToY, + value: startPoint!.quote, + pipSize: chartConfig.pipSize, + animationProgress: animationInfo.stateChangePercent, + size: size, + textStyle: config.labelStyle, + color: edgePointColor, + backgroundColor: chartTheme.backgroundColor, + ); + } + }, + drawEndPointLabel: () { + if (endPoint != null && + startPoint != null && + endPoint!.quote != startPoint!.quote) { + // Use the same color for labels as edge points + final Color edgePointColor = + config.fibonacciLevelColors['level0'] ?? config.lineStyle.color; + drawValueLabel( + canvas: canvas, + quoteToY: quoteToY, + value: endPoint!.quote, + pipSize: chartConfig.pipSize, + animationProgress: animationInfo.stateChangePercent, + size: size, + textStyle: config.labelStyle, + color: edgePointColor, + backgroundColor: chartTheme.backgroundColor, + ); + } + }, + ); + + paintXAxisLabels( + canvas, + size, + epochToX, + quoteToY, + animationInfo, + chartConfig, + chartTheme, + getDrawingState, + ); + } + + /// Paints epoch labels on the X-axis. + void paintXAxisLabels( + ui.Canvas canvas, + ui.Size size, + EpochToX epochToX, + QuoteToY quoteToY, + AnimationInfo animationInfo, + ChartConfig chartConfig, + ChartTheme chartTheme, + GetDrawingState getDrawingState, + ) { + drawLabelsWithZIndex( + canvas: canvas, + size: size, + animationInfo: animationInfo, + chartConfig: chartConfig, + chartTheme: chartTheme, + getDrawingState: getDrawingState, + drawing: this, + isDraggingStartPoint: _dragState == FibfanDragState.draggingStartPoint, + isDraggingEndPoint: _dragState == FibfanDragState.draggingEndPoint, + drawStartPointLabel: () { + if (startPoint != null) { + // Use the same color for labels as edge points + final Color edgePointColor = + config.fibonacciLevelColors['level0'] ?? config.lineStyle.color; + drawEpochLabel( + canvas: canvas, + epochToX: epochToX, + epoch: startPoint!.epoch, + size: size, + textStyle: config.labelStyle, + animationProgress: animationInfo.stateChangePercent, + color: edgePointColor, + backgroundColor: chartTheme.backgroundColor, + ); + } + }, + drawEndPointLabel: () { + if (endPoint != null && + startPoint != null && + endPoint!.epoch != startPoint!.epoch) { + // Use the same color for labels as edge points + final Color edgePointColor = + config.fibonacciLevelColors['level0'] ?? config.lineStyle.color; + drawEpochLabel( + canvas: canvas, + epochToX: epochToX, + epoch: endPoint!.epoch, + size: size, + textStyle: config.labelStyle, + animationProgress: animationInfo.stateChangePercent, + color: edgePointColor, + backgroundColor: chartTheme.backgroundColor, + ); + } + }, + ); + } + + @override + void onDragUpdate( + DragUpdateDetails details, + EpochFromX epochFromX, + QuoteFromY quoteFromY, + EpochToX epochToX, + QuoteToY quoteToY, + ) { + if (startPoint == null || endPoint == null) { + return; + } + + // Get the drag delta in screen coordinates + final Offset delta = details.delta; + + // Handle different drag states + switch (_dragState) { + case FibfanDragState.draggingStartPoint: + // Get the current screen position of the start point + final Offset currentOffset = Offset( + epochToX(startPoint!.epoch), + quoteToY(startPoint!.quote), + ); + + // Apply the delta to get the new screen position + final Offset newOffset = currentOffset + delta; + + // Convert back to epoch and quote coordinates + final int newEpoch = epochFromX(newOffset.dx); + final double newQuote = quoteFromY(newOffset.dy); + + // Update the start point + startPoint = EdgePoint( + epoch: newEpoch, + quote: newQuote, + ); + break; + + case FibfanDragState.draggingEndPoint: + // Get the current screen position of the end point + final Offset currentOffset = Offset( + epochToX(endPoint!.epoch), + quoteToY(endPoint!.quote), + ); + + // Apply the delta to get the new screen position + final Offset newOffset = currentOffset + delta; + + // Convert back to epoch and quote coordinates + final int newEpoch = epochFromX(newOffset.dx); + final double newQuote = quoteFromY(newOffset.dy); + + // Update the end point + endPoint = EdgePoint( + epoch: newEpoch, + quote: newQuote, + ); + break; + + case FibfanDragState.draggingEntireFan: + case null: + // We're dragging the whole fan + // Convert start and end points to screen coordinates + final Offset startOffset = Offset( + epochToX(startPoint!.epoch), + quoteToY(startPoint!.quote), + ); + final Offset endOffset = Offset( + epochToX(endPoint!.epoch), + quoteToY(endPoint!.quote), + ); + + // Apply the delta to get new screen coordinates + final Offset newStartOffset = startOffset + delta; + final Offset newEndOffset = endOffset + delta; + + // Convert back to epoch and quote coordinates + final int newStartEpoch = epochFromX(newStartOffset.dx); + final double newStartQuote = quoteFromY(newStartOffset.dy); + final int newEndEpoch = epochFromX(newEndOffset.dx); + final double newEndQuote = quoteFromY(newEndOffset.dy); + + // Update the start and end points + startPoint = EdgePoint( + epoch: newStartEpoch, + quote: newStartQuote, + ); + endPoint = EdgePoint( + epoch: newEndEpoch, + quote: newEndQuote, + ); + } + } + + @override + void onDragEnd( + DragEndDetails details, + EpochFromX epochFromX, + QuoteFromY quoteFromY, + EpochToX epochToX, + QuoteToY quoteToY, + ) { + // Reset the drag state when drag is complete + _dragState = null; + } + + @override + FibfanDrawingToolConfig getUpdatedConfig() => + config.copyWith(edgePoints: [ + if (startPoint != null) startPoint!, + if (endPoint != null) endPoint! + ]); + + @override + bool isInViewPort(EpochRange epochRange, QuoteRange quoteRange) => + (startPoint?.isInEpochRange( + epochRange.leftEpoch, + epochRange.rightEpoch, + ) ?? + true) || + (endPoint?.isInEpochRange( + epochRange.leftEpoch, + epochRange.rightEpoch, + ) ?? + true); + + @override + DrawingAddingPreview> + getAddingPreviewForDesktopBehaviour( + InteractiveLayerDesktopBehaviour layerBehaviour, + Function(AddingStateInfo) onAddingStateChange, + ) => + FibfanAddingPreviewDesktop( + interactiveLayerBehaviour: layerBehaviour, + interactableDrawing: this, + onAddingStateChange: onAddingStateChange, + ); + + @override + DrawingAddingPreview> + getAddingPreviewForMobileBehaviour( + InteractiveLayerMobileBehaviour layerBehaviour, + Function(AddingStateInfo) onAddingStateChange, + ) => + FibfanAddingPreviewMobile( + interactiveLayerBehaviour: layerBehaviour, + interactableDrawing: this, + onAddingStateChange: onAddingStateChange, + ); + + @override + Widget buildDrawingToolBarMenu(UpdateDrawingTool onUpdate) => Row( + children: [ + _buildLineThicknessIcon(onUpdate), + const SizedBox(width: 4), + _buildFibonacciLevelColorPicker('level0', 'level100', onUpdate), + const SizedBox(width: 4), + _buildFibonacciLevelColorPicker('level38_2', null, onUpdate), + const SizedBox(width: 4), + _buildFibonacciLevelColorPicker('level50', null, onUpdate), + const SizedBox(width: 4), + _buildFibonacciLevelColorPicker('level61_8', null, onUpdate), + ], + ); + + Widget _buildFibonacciLevelColorPicker(String levelKey, + String? secondLevelKey, UpdateDrawingTool onUpdate) => + SizedBox( + width: 32, + height: 32, + child: ColorPickerDropdownButton( + currentColor: config.fibonacciLevelColors[levelKey]!, + onColorChanged: (newColor) { + final Map updatedColors = + Map.from(config.fibonacciLevelColors); + updatedColors[levelKey] = newColor; + + // If a second level key is provided, update it as well (for top & bottom lines) + if (secondLevelKey != null) { + updatedColors[secondLevelKey] = newColor; + } + + onUpdate(config.copyWith( + fibonacciLevelColors: updatedColors, + )); + }, + ), + ); + + Widget _buildLineThicknessIcon(UpdateDrawingTool onUpdate) => + LineThicknessDropdownButton( + thickness: config.lineStyle.thickness, + onValueChanged: (double newValue) { + onUpdate(config.copyWith( + lineStyle: config.lineStyle.copyWith(thickness: newValue), + )); + }, + ); + + /// Calculates a hash of the current configuration for change detection. + /// + /// This method creates a hash based on the drawing configuration properties + /// that affect visual rendering. When the configuration changes, the hash + /// will change, triggering automatic cache management to ensure cached + /// paint objects reflect the latest styling. + /// + /// **Included Properties:** + /// - Line style (color, thickness) + /// - Fill style (color, thickness) + /// - Fibonacci level colors + /// - Label style (color, font size) + /// + /// **Returns:** Integer hash representing the current configuration state + int _calculateConfigHash() { + return Object.hash( + config.lineStyle.color.value, + config.lineStyle.thickness, + config.fillStyle.color.value, + config.fillStyle.thickness, + config.labelStyle.color?.value ?? 0, + config.labelStyle.fontSize, + config.fibonacciLevelColors.entries + .map((e) => Object.hash(e.key, e.value.value)) + .fold(0, (prev, hash) => prev ^ hash), + ); + } +} diff --git a/lib/src/deriv_chart/interactive_layer/interactable_drawings/fibfan/helpers.dart b/lib/src/deriv_chart/interactive_layer/interactable_drawings/fibfan/helpers.dart new file mode 100644 index 000000000..af404cad8 --- /dev/null +++ b/lib/src/deriv_chart/interactive_layer/interactable_drawings/fibfan/helpers.dart @@ -0,0 +1,1172 @@ +import 'dart:math' as math; + +import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/drawing_tools/data_model/drawing_paint_style.dart'; +import 'package:deriv_chart/src/theme/design_tokens/core_design_tokens.dart'; +import 'package:deriv_chart/src/theme/painting_styles/line_style.dart'; +import 'package:flutter/material.dart'; + +/// Wrapper class for cached Paint objects with timestamp tracking. +class _CachedPaint { + _CachedPaint(this.paint) : lastUsed = DateTime.now().millisecondsSinceEpoch; + + final Paint paint; + int lastUsed; + + void updateLastUsed() { + lastUsed = DateTime.now().millisecondsSinceEpoch; + } +} + +/// Wrapper class for cached TextPainter objects with timestamp tracking. +class _CachedTextPainter { + _CachedTextPainter(this.textPainter) + : lastUsed = DateTime.now().millisecondsSinceEpoch; + + final TextPainter textPainter; + int lastUsed; + + void updateLastUsed() { + lastUsed = DateTime.now().millisecondsSinceEpoch; + } +} + +/// Represents a single Fibonacci level with all its associated properties. +/// +/// This class encapsulates the ratio, label, and color key for each +/// Fibonacci retracement level, providing a more structured and +/// maintainable approach to managing level data. +@immutable +class FibonacciLevel { + /// Creates a new Fibonacci level with the specified properties. + const FibonacciLevel({ + required this.ratio, + required this.label, + required this.colorKey, + }); + + /// The mathematical ratio for this Fibonacci level (0.0 to 1.0). + final double ratio; + + /// Human-readable percentage label (e.g., "38.2%", "61.8%"). + final String label; + + /// Color key for customizable styling (e.g., "level38_2"). + final String colorKey; + + @override + String toString() => + 'FibonacciLevel(ratio: $ratio, label: $label, colorKey: $colorKey)'; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FibonacciLevel && + runtimeType == other.runtimeType && + ratio == other.ratio && + label == other.label && + colorKey == other.colorKey; + + @override + int get hashCode => ratio.hashCode ^ label.hashCode ^ colorKey.hashCode; +} + +/// Constants for Fibonacci Fan drawing operations. +/// +/// This class centralizes all magic numbers used throughout the Fibonacci Fan +/// implementation to improve maintainability and provide clear semantic meaning +/// to numerical values. +class FibfanConstants { + /// Private constructor to prevent instantiation of this utility class. + FibfanConstants._(); + + // ========== Validation Thresholds ========== + + /// Minimum meaningful delta threshold for coordinate differences. + /// + /// Used to determine if the difference between two coordinates is significant + /// enough to warrant drawing operations. Values below this threshold are + /// considered too small to be visually meaningful. + static const double defaultDeltaThreshold = 1; + + /// Threshold for detecting vertical lines to avoid division by zero. + /// + /// When the horizontal delta (deltaX) is below this threshold, the line + /// is considered vertical and special handling is applied to avoid + /// mathematical errors in slope calculations. + static const double verticalLineThreshold = 0.001; + + /// Minimum line length required for processing fan lines. + /// + /// Lines shorter than this value are skipped during hit testing and + /// other operations to avoid unnecessary calculations and potential + /// visual artifacts. + static const double minLineLength = 1; + + // ========== Mobile Positioning Constants ========== + + /// X-axis ratio for positioning the start point on mobile devices. + /// + /// Represents the fraction of screen width where the fan's start point + /// should be positioned (6% from the left edge). + static const double mobileStartXRatio = 0.06; + + /// Y-axis ratio for positioning the start point on mobile devices. + /// + /// Represents the fraction of screen height where the fan's start point + /// should be positioned (50% - center vertically). + static const double mobileStartYRatio = 0.5; + + /// X-axis ratio for positioning the end point on mobile devices. + /// + /// Represents the fraction of screen width where the fan's end point + /// should be positioned (65% from the left edge). + static const double mobileEndXRatio = 0.65; + + /// Y-axis ratio for positioning the end point on mobile devices. + /// + /// Represents the fraction of screen height where the fan's end point + /// should be positioned (30% - upper portion of screen). + static const double mobileEndYRatio = 0.3; + + /// Fallback X coordinate when screen size is unavailable on mobile. + /// + /// Used as a default horizontal position when the drawing context + /// cannot provide accurate screen dimensions. + static const double mobileFallbackX = 50; + + /// Fallback Y coordinate when screen size is unavailable on mobile. + /// + /// Used as a default vertical position when the drawing context + /// cannot provide accurate screen dimensions. Negative value places + /// the point above the start point. + static const double mobileFallbackY = -50; + + // ========== Drawing Constants ========== + + /// Width of each dash segment in dashed lines. + /// + /// Controls the length of visible segments when drawing dashed + /// preview lines in mobile mode. + static const double dashWidth = 5; + + /// Width of each space between dash segments. + /// + /// Controls the length of invisible gaps between visible segments + /// in dashed preview lines. + static const double dashSpace = 3; + + /// Opacity level for dashed preview lines. + /// + /// Applied to preview fan lines to make them visually distinct + /// from final drawn lines (70% opacity). + static const double dashOpacity = 0.7; + + /// Radius for drawing endpoint circles. + /// + /// Size of the circular indicators drawn at the start and end + /// points of the Fibonacci fan. + static const double pointRadius = 4; + + // ========== Visual Effect Constants ========== + + /// Radius of the focused circle effect around endpoints. + /// + /// Size of the glowing circle effect displayed around fan endpoints + /// when the drawing is selected or being dragged. + static const double focusedCircleRadius = 10; + + /// Stroke width of the focused circle effect. + /// + /// Thickness of the border for the glowing circle effect around + /// fan endpoints during interaction states. + static const double focusedCircleStroke = 3; + + /// Distance offset for labels from their corresponding fan lines. + /// + /// Horizontal spacing between Fibonacci level labels and their + /// associated trend lines to prevent visual overlap. + static const double labelDistanceFromLine = 5; + + /// Multiplier for positioning labels along fan lines. + /// + /// Factor used to position labels slightly beyond the fan endpoint + /// along each trend line (102% of the line length). + static const double labelPositionMultiplier = 1.02; + + /// Font size for Fibonacci level labels. + /// + /// Text size used for displaying percentage labels (0%, 38.2%, etc.) + /// next to each fan line. + static const double labelFontSize = 12; + + // ========== UI Constants ========== + + /// Size for toolbar icons (width and height). + /// + /// Dimensions for interactive elements in the drawing tool's + /// configuration toolbar (32x32 pixels). + static const double toolbarIconSize = 32; + + /// Spacing between toolbar elements. + /// + /// Horizontal gap between different controls in the drawing + /// tool's configuration toolbar. + static const double toolbarSpacing = 4; + + /// Border radius for toolbar buttons. + /// + /// Corner rounding applied to interactive buttons in the + /// drawing tool's configuration interface. + static const double toolbarBorderRadius = 4; + + /// Font size for toolbar text elements. + /// + /// Text size used for labels and values displayed in the + /// drawing tool's configuration toolbar. + static const double toolbarFontSize = 14; + + /// Line height multiplier for toolbar text. + /// + /// Vertical spacing factor applied to text elements in the + /// toolbar to ensure proper vertical alignment. + static const double toolbarTextHeight = 2; + + // ========== Coordinate Defaults ========== + + /// Default coordinate value for fallback scenarios. + /// + /// Used as a safe default when coordinate calculations fail + /// or when initializing coordinate values. + static const double defaultCoordinate = 0; +} + +/// Helper class for Fibonacci Fan drawing operations. +/// +/// This class provides static methods and constants for drawing Fibonacci Fan +/// technical analysis tools on charts. Fibonacci fans are used to identify +/// potential support and resistance levels based on Fibonacci ratios. +/// +/// The fan consists of multiple trend lines drawn from a base point, each +/// representing different Fibonacci retracement levels (0%, 38.2%, 50%, 61.8%, 100%). +/// +/// **Performance Optimization:** +/// This class implements paint object caching to improve rendering performance +/// by reusing Paint objects instead of creating new ones for each draw operation. +/// The cache is automatically managed to prevent memory bloat through: +/// - Size-based eviction when cache exceeds maximum entries +/// - Time-based expiration for unused cache entries +/// - Configuration change detection for selective invalidation +class FibonacciFanHelpers { + /// Maximum number of entries allowed in each cache before eviction occurs. + static const int _maxCacheSize = 100; + + /// Time in milliseconds after which unused cache entries expire. + static const int _cacheExpirationMs = 300000; // 5 minutes + + /// Cache for line paint objects to improve performance. + /// + /// Maps paint configuration keys to reusable Paint objects. This prevents + /// the overhead of creating new Paint objects for each drawing operation, + /// which can significantly improve performance during animations and + /// frequent redraws. + /// + /// **Cache Key Format:** `"line_${color.value}_${thickness}"` + static final Map _linePaintCache = + {}; + + /// Cache for fill paint objects to improve performance. + /// + /// Maps paint configuration keys to reusable Paint objects for fill operations. + /// This is particularly beneficial for drawing the filled areas between + /// fan lines, which can involve multiple fill operations per frame. + /// + /// **Cache Key Format:** `"fill_${color.value}_${thickness}"` + static final Map _fillPaintCache = + {}; + + /// Cache for dash paint objects to improve performance. + /// + /// Maps paint configuration keys to reusable Paint objects for dashed lines. + /// Used primarily in mobile preview mode where dashed lines are drawn + /// frequently during user interactions. + /// + /// **Cache Key Format:** `"dash_${color.value}_${thickness}_${opacity}"` + static final Map _dashPaintCache = + {}; + + /// Cache for text painter objects to improve label rendering performance. + /// + /// Maps text configuration keys to reusable TextPainter objects. This is + /// especially beneficial for Fibonacci level labels which are drawn + /// repeatedly with the same styling. + /// + /// **Cache Key Format:** `"text_${text}_${color.value}_${fontSize}_${fontWeight.index}_${fontFamily}"` + static final Map _textPainterCache = + {}; + + /// Tracks the last configuration hash to detect changes. + static int? _lastConfigHash; + + /// Gets or creates a cached line paint object. + /// + /// Returns a reusable Paint object configured for line drawing. If a paint + /// object with the same configuration already exists in the cache, it is + /// returned. Otherwise, a new one is created, cached, and returned. + /// + /// **Parameters:** + /// - [color]: Line color + /// - [thickness]: Line thickness + /// + /// **Returns:** Cached or newly created Paint object for line drawing + /// + /// **Performance Benefit:** Eliminates Paint object allocation overhead + /// during frequent drawing operations, especially during animations. + static Paint getCachedLinePaint(Color color, double thickness) { + _performAutomaticCacheManagement(); + final String key = 'line_${color.value}_$thickness'; + final cachedPaint = _linePaintCache.putIfAbsent( + key, + () => _CachedPaint(Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = thickness)) + ..updateLastUsed(); + return cachedPaint.paint; + } + + /// Gets or creates a cached fill paint object. + /// + /// Returns a reusable Paint object configured for fill operations. This is + /// particularly useful for drawing the filled areas between fan lines. + /// + /// **Parameters:** + /// - [color]: Fill color + /// - [thickness]: Stroke thickness (for fill border if applicable) + /// + /// **Returns:** Cached or newly created Paint object for fill operations + /// + /// **Performance Benefit:** Reduces memory allocation during fill operations, + /// which can be frequent when drawing multiple fan fill areas. + static Paint getCachedFillPaint(Color color, double thickness) { + _performAutomaticCacheManagement(); + final String key = 'fill_${color.value}_$thickness'; + final cachedPaint = _fillPaintCache.putIfAbsent( + key, + () => _CachedPaint(Paint() + ..color = color + ..style = PaintingStyle.fill + ..strokeWidth = thickness)) + ..updateLastUsed(); + return cachedPaint.paint; + } + + /// Gets or creates a cached dash paint object. + /// + /// Returns a reusable Paint object configured for dashed line drawing. + /// Used primarily in mobile preview mode for drawing dashed fan lines. + /// + /// **Parameters:** + /// - [color]: Dash line color + /// - [thickness]: Dash line thickness + /// - [opacity]: Dash line opacity (0.0 to 1.0) + /// + /// **Returns:** Cached or newly created Paint object for dashed lines + /// + /// **Performance Benefit:** Optimizes mobile preview performance where + /// dashed lines are drawn frequently during user interactions. + static Paint getCachedDashPaint( + Color color, double thickness, double opacity) { + _performAutomaticCacheManagement(); + final String key = 'dash_${color.value}_${thickness}_$opacity'; + final cachedPaint = _dashPaintCache.putIfAbsent( + key, + () => _CachedPaint(Paint() + ..color = color.withOpacity(opacity) + ..style = PaintingStyle.stroke + ..strokeWidth = thickness)) + ..updateLastUsed(); + return cachedPaint.paint; + } + + /// Gets or creates a cached text painter object. + /// + /// Returns a reusable TextPainter object configured for text rendering. + /// This is especially beneficial for Fibonacci level labels which use + /// consistent styling and are drawn repeatedly. + /// + /// **Parameters:** + /// - [text]: Text content to render + /// - [color]: Text color + /// - [fontSize]: Text font size + /// + /// **Returns:** Cached or newly created TextPainter object + /// + /// **Performance Benefit:** Eliminates TextPainter creation and layout + /// overhead for repeated label rendering, significantly improving + /// performance during animations and frequent redraws. + static TextPainter getCachedTextPainter( + String text, Color color, double fontSize, + {FontWeight? fontWeight, String? fontFamily}) { + _performAutomaticCacheManagement(); + + // Include font weight and family in cache key to prevent incorrect rendering + final FontWeight effectiveFontWeight = fontWeight ?? FontWeight.w500; + final String effectiveFontFamily = fontFamily ?? ''; + final String key = + 'text_${text}_${color.value}_${fontSize}_${effectiveFontWeight.index}_$effectiveFontFamily'; + + final cachedTextPainter = _textPainterCache.putIfAbsent(key, () { + final textPainter = TextPainter( + text: TextSpan( + text: text, + style: TextStyle( + color: color, + fontSize: fontSize, + fontWeight: effectiveFontWeight, + fontFamily: fontFamily, + ), + ), + textDirection: TextDirection.ltr, + )..layout(); + return _CachedTextPainter(textPainter); + }) + ..updateLastUsed(); + return cachedTextPainter.textPainter; + } + + /// Performs automatic cache management including size-based eviction and time-based expiration. + /// + /// This method is called automatically by cache getter methods to ensure + /// memory usage stays within acceptable bounds. It implements: + /// - Size-based eviction: Removes oldest entries when cache exceeds maximum size + /// - Time-based expiration: Removes entries that haven't been used recently + /// + /// **Performance Note:** This method is designed to be lightweight and only + /// performs cleanup when necessary to avoid impacting drawing performance. + static void _performAutomaticCacheManagement() { + final int currentTime = DateTime.now().millisecondsSinceEpoch; + + // Perform size-based eviction for each cache + _evictOldestEntries(_linePaintCache); + _evictOldestEntries(_fillPaintCache); + _evictOldestEntries(_dashPaintCache); + _evictOldestEntriesTextPainter(_textPainterCache); + + // Perform time-based expiration + _expireOldEntries(_linePaintCache, currentTime); + _expireOldEntries(_fillPaintCache, currentTime); + _expireOldEntries(_dashPaintCache, currentTime); + _expireOldEntriesTextPainter(_textPainterCache, currentTime); + } + + /// Evicts oldest entries from paint cache when size limit is exceeded. + static void _evictOldestEntries(Map cache) { + if (cache.length <= _maxCacheSize) { + return; + } + + final entries = cache.entries.toList() + ..sort((a, b) => a.value.lastUsed.compareTo(b.value.lastUsed)); + + final entriesToRemove = cache.length - _maxCacheSize; + for (int i = 0; i < entriesToRemove; i++) { + cache.remove(entries[i].key); + } + } + + /// Evicts oldest entries from text painter cache when size limit is exceeded. + static void _evictOldestEntriesTextPainter( + Map cache) { + if (cache.length <= _maxCacheSize) { + return; + } + + final entries = cache.entries.toList() + ..sort((a, b) => a.value.lastUsed.compareTo(b.value.lastUsed)); + + final entriesToRemove = cache.length - _maxCacheSize; + for (int i = 0; i < entriesToRemove; i++) { + cache.remove(entries[i].key); + } + } + + /// Removes expired entries from paint cache based on time threshold. + static void _expireOldEntries( + Map cache, int currentTime) { + cache.removeWhere( + (key, value) => currentTime - value.lastUsed > _cacheExpirationMs); + } + + /// Removes expired entries from text painter cache based on time threshold. + static void _expireOldEntriesTextPainter( + Map cache, int currentTime) { + cache.removeWhere( + (key, value) => currentTime - value.lastUsed > _cacheExpirationMs); + } + + /// Detects configuration changes and clears relevant cache entries. + /// + /// This method should be called when drawing configuration changes to ensure + /// cached objects reflect the latest styling. It compares the current + /// configuration hash with the previous one and performs selective cache + /// invalidation when changes are detected. + /// + /// **Parameters:** + /// - [configHash]: Hash of the current configuration + /// + /// **Use Cases:** + /// - Theme changes that affect colors or styling + /// - User customization of drawing properties + /// - Dynamic style updates during runtime + static void handleConfigurationChange(int configHash) { + if (_lastConfigHash != null && _lastConfigHash != configHash) { + // Configuration has changed, perform selective cache clearing + // For now, clear all caches to ensure consistency + // In the future, this could be made more granular based on what changed + clearPaintCaches(); + } + _lastConfigHash = configHash; + } + + /// Clears all paint and text painter caches. + /// + /// This method should be called when memory optimization is needed or + /// when the drawing configuration has changed significantly. It's + /// recommended to call this during major theme changes or when the + /// drawing tool is no longer in use. + /// + /// **Use Cases:** + /// - Memory cleanup during app lifecycle events + /// - Theme changes that invalidate cached paint objects + /// - Drawing tool deactivation + /// + /// **Performance Note:** After clearing caches, the next drawing operations + /// will recreate paint objects, so avoid calling this during active drawing. + static void clearPaintCaches() { + _linePaintCache.clear(); + _fillPaintCache.clear(); + _dashPaintCache.clear(); + _textPainterCache.clear(); + } + + /// Clears only the text painter cache. + /// + /// This method provides selective cache invalidation for text painters + /// without affecting paint object caches. Use this when text styling + /// changes but paint configurations remain the same. + /// + /// **Use Cases:** + /// - Font size changes in user preferences + /// - Text color theme updates + /// - Label content modifications + /// - Localization changes affecting label text + /// + /// **Performance Benefit:** Preserves paint object caches while ensuring + /// text rendering reflects the latest styling changes. + static void clearTextPainterCache() { + _textPainterCache.clear(); + } + + /// Clears text painter cache entries for a specific color. + /// + /// Provides targeted cache invalidation when only specific color + /// configurations have changed. This is more efficient than clearing + /// the entire text painter cache. + /// + /// **Parameters:** + /// - [color]: The color for which to clear cached text painters + /// + /// **Use Cases:** + /// - Single color theme updates + /// - User customization of specific Fibonacci level colors + /// - Selective color scheme changes + /// + /// **Performance Benefit:** Preserves text painters with other colors, + /// reducing the need to recreate unaffected cache entries. + static void clearTextPainterCacheForColor(Color color) { + _textPainterCache.removeWhere((key, _) => key.contains('_${color.value}_')); + } + + /// Clears text painter cache entries for a specific font size. + /// + /// Provides targeted cache invalidation when font size preferences + /// change. This is more efficient than clearing the entire cache + /// when only font size has been modified. + /// + /// **Parameters:** + /// - [fontSize]: The font size for which to clear cached text painters + /// + /// **Use Cases:** + /// - User font size preference changes + /// - Accessibility font scaling updates + /// - Dynamic font size adjustments + /// + /// **Performance Benefit:** Preserves text painters with other font sizes, + /// maintaining cache efficiency for unaffected configurations. + static void clearTextPainterCacheForFontSize(double fontSize) { + _textPainterCache.removeWhere((key, _) => key.contains('_${fontSize}_')); + } + + /// Clears text painter cache entries for a specific font weight. + /// + /// Provides targeted cache invalidation when font weight preferences + /// change. This is more efficient than clearing the entire cache + /// when only font weight has been modified. + /// + /// **Parameters:** + /// - [fontWeight]: The font weight for which to clear cached text painters + /// + /// **Use Cases:** + /// - User font weight preference changes + /// - Theme updates affecting text weight + /// - Dynamic font weight adjustments + /// + /// **Performance Benefit:** Preserves text painters with other font weights, + /// maintaining cache efficiency for unaffected configurations. + static void clearTextPainterCacheForFontWeight(FontWeight fontWeight) { + _textPainterCache + .removeWhere((key, _) => key.contains('_${fontWeight.index}_')); + } + + /// Clears text painter cache entries for a specific font family. + /// + /// Provides targeted cache invalidation when font family preferences + /// change. This is more efficient than clearing the entire cache + /// when only font family has been modified. + /// + /// **Parameters:** + /// - [fontFamily]: The font family for which to clear cached text painters + /// + /// **Use Cases:** + /// - User font family preference changes + /// - Theme updates affecting font family + /// - Dynamic font family adjustments + /// + /// **Performance Benefit:** Preserves text painters with other font families, + /// maintaining cache efficiency for unaffected configurations. + static void clearTextPainterCacheForFontFamily(String fontFamily) { + _textPainterCache.removeWhere((key, _) => key.endsWith('_$fontFamily')); + } + + /// Clears only paint object caches, preserving text painter cache. + /// + /// This method provides selective cache invalidation for paint objects + /// without affecting text painter caches. Use this when paint styling + /// changes but text configurations remain the same. + /// + /// **Use Cases:** + /// - Line thickness changes + /// - Paint color updates (non-text) + /// - Stroke style modifications + /// - Fill opacity adjustments + /// + /// **Performance Benefit:** Preserves text painter caches while ensuring + /// paint rendering reflects the latest styling changes. + static void clearPaintObjectCaches() { + _linePaintCache.clear(); + _fillPaintCache.clear(); + _dashPaintCache.clear(); + } + + /// Gets cache statistics for performance monitoring. + /// + /// Returns information about the current state of all paint caches. + /// This can be useful for performance monitoring and optimization. + /// + /// **Returns:** Map containing cache sizes and memory usage information + /// + /// **Example:** + /// ```dart + /// final stats = FibonacciFanHelpers.getCacheStats(); + /// print('Line paint cache size: ${stats['linePaintCacheSize']}'); + /// ``` + static Map getCacheStats() { + return { + 'linePaintCacheSize': _linePaintCache.length, + 'fillPaintCacheSize': _fillPaintCache.length, + 'dashPaintCacheSize': _dashPaintCache.length, + 'textPainterCacheSize': _textPainterCache.length, + }; + } + + /// Validates that all coordinates in the given offsets are not NaN. + /// + /// This method checks a list of [Offset] objects to ensure that both + /// their x and y coordinates are valid numbers (not NaN). This is + /// essential for preventing rendering errors when drawing operations + /// encounter invalid coordinate data. + /// + /// **Parameters:** + /// - [offsets]: List of coordinate points to validate + /// + /// **Returns:** + /// - `true` if all coordinates are valid (not NaN) + /// - `false` if any coordinate contains NaN values + /// + /// **Example:** + /// ```dart + /// final points = [Offset(10, 20), Offset(30, 40)]; + /// if (FibonacciFanHelpers.areCoordinatesValid(points)) { + /// // Safe to proceed with drawing operations + /// } + /// ``` + static bool areCoordinatesValid(List offsets) { + return offsets.every((offset) => !offset.dx.isNaN && !offset.dy.isNaN); + } + + /// Validates that a single offset has valid coordinates. + /// + /// Checks whether both x and y coordinates of an [Offset] are valid + /// numbers (not NaN). This is a fundamental validation used throughout + /// the drawing operations to prevent mathematical errors. + /// + /// **Parameters:** + /// - [offset]: The coordinate point to validate + /// + /// **Returns:** + /// - `true` if both x and y coordinates are valid numbers + /// - `false` if either coordinate is NaN + /// + /// **Example:** + /// ```dart + /// final point = Offset(mouseX, mouseY); + /// if (FibonacciFanHelpers.isOffsetValid(point)) { + /// // Safe to use this point for calculations + /// } + /// ``` + static bool isOffsetValid(Offset offset) { + return !offset.dx.isNaN && !offset.dy.isNaN; + } + + /// Validates that two offsets have valid coordinates. + /// + /// Convenience method that checks both offsets for coordinate validity. + /// This is commonly used when validating start and end points before + /// performing line drawing or geometric calculations. + /// + /// **Parameters:** + /// - [offset1]: First coordinate point to validate + /// - [offset2]: Second coordinate point to validate + /// + /// **Returns:** + /// - `true` if both offsets have valid coordinates + /// - `false` if either offset contains NaN values + /// + /// **Example:** + /// ```dart + /// if (FibonacciFanHelpers.areTwoOffsetsValid(startPoint, endPoint)) { + /// // Safe to draw line between these points + /// } + /// ``` + static bool areTwoOffsetsValid(Offset offset1, Offset offset2) { + return isOffsetValid(offset1) && isOffsetValid(offset2); + } + + /// Validates that coordinate deltas are meaningful (not too small). + /// + /// Determines whether the difference between two coordinates is large + /// enough to warrant drawing operations. Very small deltas can cause + /// visual artifacts or unnecessary computational overhead. + /// + /// **Parameters:** + /// - [deltaX]: Horizontal coordinate difference + /// - [deltaY]: Vertical coordinate difference + /// - [threshold]: Minimum meaningful difference (defaults to [FibfanConstants.defaultDeltaThreshold]) + /// + /// **Returns:** + /// - `true` if either delta exceeds the threshold + /// - `false` if both deltas are below the threshold + /// + /// **Example:** + /// ```dart + /// final deltaX = endPoint.dx - startPoint.dx; + /// final deltaY = endPoint.dy - startPoint.dy; + /// if (FibonacciFanHelpers.areDeltasMeaningful(deltaX, deltaY)) { + /// // Proceed with drawing the fan + /// } + /// ``` + static bool areDeltasMeaningful(double deltaX, double deltaY, + {double threshold = FibfanConstants.defaultDeltaThreshold}) { + return deltaX.abs() > threshold || deltaY.abs() > threshold; + } + + /// Fibonacci levels in the desired visual order for drawing operations. + /// + /// This list defines all Fibonacci retracement levels used in the fan, + /// ordered from flattest to steepest for proper visual layering. + /// Each level contains its ratio, display label, and color key. + /// + /// **Visual Order (bottom to top):** + /// 1. 0% (0.0) - Baseline level (horizontal, flattest) + /// 2. 38.2% (0.382) - First major retracement level + /// 3. 50% (0.5) - Midpoint retracement (commonly used) + /// 4. 61.8% (0.618) - Golden ratio retracement level + /// 5. 100% (1.0) - Steepest line, full retracement + /// + /// **Benefits of this approach:** + /// - Single source of truth for all level data + /// - Consistent ordering across all operations + /// - Type-safe access to level properties + /// - Easy to add/remove/modify levels + /// - Eliminates data duplication + static const List fibonacciLevels = [ + FibonacciLevel(ratio: 0, label: '0%', colorKey: 'level0'), + FibonacciLevel(ratio: 0.382, label: '38.2%', colorKey: 'level38_2'), + FibonacciLevel(ratio: 0.5, label: '50%', colorKey: 'level50'), + FibonacciLevel(ratio: 0.618, label: '61.8%', colorKey: 'level61_8'), + FibonacciLevel(ratio: 1, label: '100%', colorKey: 'level100'), + ]; + + /// Gets a Fibonacci level by its ratio value. + /// + /// Provides convenient access to level data when you have the ratio. + /// Returns null if no level with the specified ratio exists. + /// + /// **Parameters:** + /// - [ratio]: The mathematical ratio to search for + /// + /// **Returns:** The matching FibonacciLevel or null if not found + /// + /// **Example:** + /// ```dart + /// final goldenLevel = FibonacciFanHelpers.getLevelByRatio(0.618); + /// print(goldenLevel?.label); // "61.8%" + /// ``` + static FibonacciLevel? getLevelByRatio(double ratio) { + try { + return fibonacciLevels.firstWhere((level) => level.ratio == ratio); + } on StateError { + return null; + } + } + + /// Gets a Fibonacci level by its color key. + /// + /// Provides convenient access to level data when you have the color key. + /// Returns null if no level with the specified color key exists. + /// + /// **Parameters:** + /// - [colorKey]: The color key to search for + /// + /// **Returns:** The matching FibonacciLevel or null if not found + /// + /// **Example:** + /// ```dart + /// final level = FibonacciFanHelpers.getLevelByColorKey('level61_8'); + /// print(level?.ratio); // 0.618 + /// ``` + static FibonacciLevel? getLevelByColorKey(String colorKey) { + try { + return fibonacciLevels.firstWhere((level) => level.colorKey == colorKey); + } on StateError { + return null; + } + } + + /// Calculates the triangular polygons for each fan area between adjacent Fibonacci levels. + /// + /// This method extracts the shared geometric calculations used by both hit testing + /// and drawing operations, eliminating code duplication and ensuring consistency. + /// Each polygon represents the area between two adjacent Fibonacci fan lines. + /// + /// **Algorithm:** + /// 1. Calculates the base angle from start to end point + /// 2. For each pair of adjacent Fibonacci ratios, calculates their angles + /// 3. Extends lines to screen edges using trigonometric calculations + /// 4. Validates coordinates and creates triangular polygons + /// + /// **Parameters:** + /// - [startOffset]: Starting point of the fan in screen coordinates + /// - [deltaX]: Horizontal distance from start to end point + /// - [deltaY]: Vertical distance from start to end point + /// - [size]: Canvas size for boundary calculations + /// + /// **Returns:** List of triangular areas, where each triangle is defined by three points: + /// [startOffset, extendedPoint1, extendedPoint2] + static List> calculateFanAreaPolygons({ + required Offset startOffset, + required double deltaX, + required double deltaY, + required Size size, + }) { + final List> polygons = []; + + // Calculate the base angle from start to end point + final double baseAngle = math.atan2(deltaY, deltaX); + + // Calculate screen edge distance + final double screenWidth = size.width; + final double distanceToEdge = screenWidth - startOffset.dx; + + // Generate polygons for each fan area + for (int i = 0; i < fibonacciLevels.length - 1; i++) { + final double ratio1 = fibonacciLevels[i].ratio; + final double ratio2 = fibonacciLevels[i + 1].ratio; + + // Calculate angles + const double horizontalAngle = 0; + final double angle1 = baseAngle + (horizontalAngle - baseAngle) * ratio1; + final double angle2 = baseAngle + (horizontalAngle - baseAngle) * ratio2; + + // Calculate extended points + final Offset extendedPoint1 = Offset( + screenWidth, + startOffset.dy + distanceToEdge * math.tan(angle1), + ); + final Offset extendedPoint2 = Offset( + screenWidth, + startOffset.dy + distanceToEdge * math.tan(angle2), + ); + + // Validate coordinates + if (areCoordinatesValid([startOffset, extendedPoint1, extendedPoint2])) { + polygons.add([startOffset, extendedPoint1, extendedPoint2]); + } + } + + return polygons; + } + + /// Draws the filled areas between fan lines using angle-based calculations. + /// + /// Creates alternating filled regions between adjacent Fibonacci fan lines + /// to provide visual distinction between different retracement levels. + /// The fill areas help users identify price zones more easily. + /// + /// **Algorithm:** + /// 1. Uses shared polygon calculation method for consistency + /// 2. Creates triangular fill paths between adjacent lines + /// 3. Applies fill styling with appropriate opacity + /// + /// **Parameters:** + /// - [canvas]: The drawing canvas + /// - [startOffset]: Starting point of the fan in screen coordinates + /// - [deltaX]: Horizontal distance from start to end point + /// - [deltaY]: Vertical distance from start to end point + /// - [size]: Canvas size for boundary calculations + /// - [paintStyle]: Paint style configuration + /// - [fillStyle]: Fill style and color configuration + /// - [fibonacciLevelColors]: Optional custom colors for each level + /// + /// **Visual Effect:** + /// - Uses consistent polygon calculations with hit testing + /// - Creates alternating light/lighter pattern + static void drawFanFills( + Canvas canvas, + Offset startOffset, + double deltaX, + double deltaY, + Size size, + DrawingPaintStyle paintStyle, + LineStyle fillStyle, { + Map? fibonacciLevelColors, + }) { + // Use shared calculation method for consistency with hit testing + final List> fanPolygons = calculateFanAreaPolygons( + startOffset: startOffset, + deltaX: deltaX, + deltaY: deltaY, + size: size, + ); + + // Draw each calculated polygon + for (final List polygon in fanPolygons) { + // Create path for the filled area + final Path fillPath = Path() + ..moveTo(polygon[0].dx, polygon[0].dy) + ..lineTo(polygon[1].dx, polygon[1].dy) + ..lineTo(polygon[2].dx, polygon[2].dy) + ..close(); + + // Use level0 color from fibonacciLevelColors if available, otherwise use fillStyle color + final Color fillColor = (fibonacciLevelColors != null && + fibonacciLevelColors.containsKey('level0')) + ? fibonacciLevelColors['level0']! + : fillStyle.color; + + // Create custom fill paint with opacity + final Paint fillPaint = getCachedFillPaint( + fillColor.withOpacity(CoreDesignTokens.coreOpacity100), + fillStyle.thickness); + + canvas.drawPath(fillPath, fillPaint); + } + } + + /// Draws the fan lines representing Fibonacci retracement levels using angle-based calculations. + /// + /// Creates the main trend lines of the Fibonacci fan, each representing + /// a different retracement level. Lines extend from the start point to + /// the screen edge, with each line angled according to its Fibonacci ratio + /// as a percentage of the base angle. + /// + /// **Algorithm:** + /// 1. Calculates the base angle from start to end point + /// 2. For each Fibonacci ratio, calculates the angle as a percentage of the base angle + /// 3. Determines line color (custom or default) + /// 4. Extends line to screen edge using trigonometric calculations + /// 5. Draws the line with appropriate styling + /// + /// **Parameters:** + /// - [canvas]: The drawing canvas + /// - [startOffset]: Starting point of the fan in screen coordinates + /// - [deltaX]: Horizontal distance from start to end point + /// - [deltaY]: Vertical distance from start to end point + /// - [size]: Canvas size for boundary calculations + /// - [paintStyle]: Paint style configuration + /// - [lineStyle]: Default line style and color + /// - [fibonacciLevelColors]: Optional custom colors for each level + /// + /// **Angle-Based Approach:** + /// - 0%: Horizontal line (0 degrees) + /// - 38.2%: 38.2% of the base angle + /// - 50%: 50% of the base angle + /// - 61.8%: 61.8% of the base angle + /// - 100%: Full base angle (same as original trend line) + /// + /// **Color Mapping:** + /// - Uses custom colors from [fibonacciLevelColors] if provided + /// - Falls back to default [lineStyle.color] if no custom color exists + static void drawFanLines( + Canvas canvas, + Offset startOffset, + double deltaX, + double deltaY, + Size size, + DrawingPaintStyle paintStyle, + LineStyle lineStyle, { + Map? fibonacciLevelColors, + }) { + // Calculate the base angle from start to end point + final double baseAngle = math.atan2(deltaY, deltaX); + + for (int i = 0; i < fibonacciLevels.length; i++) { + final FibonacciLevel level = fibonacciLevels[i]; + final Color lineColor = (fibonacciLevelColors != null && + fibonacciLevelColors.containsKey(level.colorKey)) + ? fibonacciLevelColors[level.colorKey]! + : lineStyle.color; + + final Paint linePaint = + getCachedLinePaint(lineColor, lineStyle.thickness); + + // Calculate angle: 0% should point to end point, 100% should be horizontal (0 degrees) + // Interpolate between the end angle (baseAngle) and horizontal reference (0 degrees) + const double horizontalAngle = 0; // Horizontal reference + final double fanAngle = + baseAngle + (horizontalAngle - baseAngle) * level.ratio; + + // Extend line to the edge of the screen using angle-based calculations + final double screenWidth = size.width; + final double distanceToEdge = screenWidth - startOffset.dx; + + // Calculate extended point using trigonometry + final Offset extendedPoint = Offset( + screenWidth, + startOffset.dy + distanceToEdge * math.tan(fanAngle), + ); + + // Validate coordinates before drawing + if (areTwoOffsetsValid(startOffset, extendedPoint)) { + canvas.drawLine(startOffset, extendedPoint, linePaint); + } + } + } + + /// Draws labels for the fan lines showing Fibonacci percentages using angle-based calculations. + /// + /// Places percentage labels (0%, 38.2%, 50%, 61.8%, 100%) next to their + /// corresponding fan lines. Labels are rotated to align with their respective + /// lines and positioned slightly beyond the fan endpoint for clarity. + /// + /// **Algorithm:** + /// 1. Calculates the base angle from start to end point + /// 2. For each Fibonacci ratio, calculates the angle as a percentage of the base angle + /// 3. Positions label along the calculated angle + /// 4. Applies canvas transformations (translate + rotate) + /// 5. Draws the rotated text with appropriate styling + /// 6. Restores canvas state for next label + /// + /// **Parameters:** + /// - [canvas]: The drawing canvas + /// - [startOffset]: Starting point of the fan in screen coordinates + /// - [deltaX]: Horizontal distance from start to end point + /// - [deltaY]: Vertical distance from start to end point + /// - [size]: Canvas size for boundary calculations + /// - [lineStyle]: Default line style for fallback color + /// - [fibonacciLevelColors]: Optional custom colors for each level + /// + /// **Label Positioning:** + /// - Position: Along each fan line at a fixed distance from start point + /// - Rotation: Aligned with the angle of the corresponding fan line + /// - Offset: 5 pixels from the line to prevent overlap + /// - Font: 12px medium weight for readability + /// + /// **Color Mapping:** + /// - Uses custom colors from [fibonacciLevelColors] if provided + /// - Falls back to default [lineStyle.color] if no custom color exists + static void drawFanLabels( + Canvas canvas, + Offset startOffset, + double deltaX, + double deltaY, + Size size, + LineStyle lineStyle, { + Map? fibonacciLevelColors, + }) { + // Calculate the base angle from start to end point + final double baseAngle = math.atan2(deltaY, deltaX); + + // Calculate a fixed distance for label positioning + final double labelDistance = math.sqrt(deltaX * deltaX + deltaY * deltaY) * + FibfanConstants.labelPositionMultiplier; + + for (int i = 0; i < FibonacciFanHelpers.fibonacciLevels.length; i++) { + final FibonacciLevel level = FibonacciFanHelpers.fibonacciLevels[i]; + + // Calculate angle: 0% should point to end point, 100% should be horizontal (0 degrees) + // Interpolate between the end angle (baseAngle) and horizontal reference (0 degrees) + const double horizontalAngle = 0; // Horizontal reference + final double fanAngle = + baseAngle + (horizontalAngle - baseAngle) * level.ratio; + + // Calculate label position along the fan line + final Offset labelPosition = Offset( + startOffset.dx + labelDistance * math.cos(fanAngle), + startOffset.dy + labelDistance * math.sin(fanAngle), + ); + + // Use custom color if provided, otherwise use default line style color + final Color labelColor = (fibonacciLevelColors != null && + fibonacciLevelColors.containsKey(level.colorKey)) + ? fibonacciLevelColors[level.colorKey]! + : lineStyle.color; + + final TextPainter textPainter = getCachedTextPainter( + level.label, + labelColor, + FibfanConstants.labelFontSize, + ); + + // Save the current canvas state + canvas + ..save() + // Translate to the label position + ..translate(labelPosition.dx, labelPosition.dy) + // Rotate the canvas by the fan angle + ..rotate(fanAngle); + + // Adjust text position to left-align it when rotated + final Offset textOffset = Offset( + FibfanConstants.labelDistanceFromLine, // Small offset from the line + -textPainter.height, + ); + + // Draw the rotated text + textPainter.paint(canvas, textOffset); + + // Restore the canvas state + canvas.restore(); + } + } +} diff --git a/lib/src/deriv_chart/interactive_layer/interactable_drawings/trend_line/trend_line_interactable_drawing.dart b/lib/src/deriv_chart/interactive_layer/interactable_drawings/trend_line/trend_line_interactable_drawing.dart index 7d18466ea..e69c09797 100644 --- a/lib/src/deriv_chart/interactive_layer/interactable_drawings/trend_line/trend_line_interactable_drawing.dart +++ b/lib/src/deriv_chart/interactive_layer/interactable_drawings/trend_line/trend_line_interactable_drawing.dart @@ -386,39 +386,50 @@ class TrendLineInteractableDrawing ChartTheme chartTheme, GetDrawingState getDrawingState, ) { - if (getDrawingState(this).contains(DrawingToolState.selected)) { - // Draw value label for start point - if (startPoint != null) { - drawValueLabel( - canvas: canvas, - quoteToY: quoteToY, - value: startPoint!.quote, - pipSize: chartConfig.pipSize, - animationProgress: animationInfo.stateChangePercent, - size: size, - textStyle: config.labelStyle, - color: config.lineStyle.color, - backgroundColor: chartTheme.backgroundColor, - ); - } + drawLabelsWithZIndex( + canvas: canvas, + size: size, + animationInfo: animationInfo, + chartConfig: chartConfig, + chartTheme: chartTheme, + getDrawingState: getDrawingState, + drawing: this, + isDraggingStartPoint: isDraggingStartPoint == true, + isDraggingEndPoint: isDraggingStartPoint == false, + drawStartPointLabel: () { + if (startPoint != null) { + drawValueLabel( + canvas: canvas, + quoteToY: quoteToY, + value: startPoint!.quote, + pipSize: chartConfig.pipSize, + animationProgress: animationInfo.stateChangePercent, + size: size, + textStyle: config.labelStyle, + color: config.lineStyle.color, + backgroundColor: chartTheme.backgroundColor, + ); + } + }, + drawEndPointLabel: () { + if (endPoint != null && + startPoint != null && + endPoint!.quote != startPoint!.quote) { + drawValueLabel( + canvas: canvas, + quoteToY: quoteToY, + value: endPoint!.quote, + pipSize: chartConfig.pipSize, + animationProgress: animationInfo.stateChangePercent, + size: size, + textStyle: config.labelStyle, + color: config.lineStyle.color, + backgroundColor: chartTheme.backgroundColor, + ); + } + }, + ); - // Draw value label for end point (offset slightly to avoid overlap) - if (endPoint != null && - startPoint != null && - endPoint!.quote != startPoint!.quote) { - drawValueLabel( - canvas: canvas, - quoteToY: quoteToY, - value: endPoint!.quote, - pipSize: chartConfig.pipSize, - animationProgress: animationInfo.stateChangePercent, - size: size, - textStyle: config.labelStyle, - color: config.lineStyle.color, - backgroundColor: chartTheme.backgroundColor, - ); - } - } // Paint X-axis labels when selected paintXAxisLabels( canvas, @@ -443,37 +454,47 @@ class TrendLineInteractableDrawing ChartTheme chartTheme, GetDrawingState getDrawingState, ) { - if (getDrawingState(this).contains(DrawingToolState.selected)) { - // Draw epoch label for start point - if (startPoint != null) { - drawEpochLabel( - canvas: canvas, - epochToX: epochToX, - epoch: startPoint!.epoch, - size: size, - textStyle: config.labelStyle, - animationProgress: animationInfo.stateChangePercent, - color: config.lineStyle.color, - backgroundColor: chartTheme.backgroundColor, - ); - } - - // Draw epoch label for end point (only if different from start point to avoid overlap) - if (endPoint != null && - startPoint != null && - endPoint!.epoch != startPoint!.epoch) { - drawEpochLabel( - canvas: canvas, - epochToX: epochToX, - epoch: endPoint!.epoch, - size: size, - textStyle: config.labelStyle, - animationProgress: animationInfo.stateChangePercent, - color: config.lineStyle.color, - backgroundColor: chartTheme.backgroundColor, - ); - } - } + drawLabelsWithZIndex( + canvas: canvas, + size: size, + animationInfo: animationInfo, + chartConfig: chartConfig, + chartTheme: chartTheme, + getDrawingState: getDrawingState, + drawing: this, + isDraggingStartPoint: isDraggingStartPoint == true, + isDraggingEndPoint: isDraggingStartPoint == false, + drawStartPointLabel: () { + if (startPoint != null) { + drawEpochLabel( + canvas: canvas, + epochToX: epochToX, + epoch: startPoint!.epoch, + size: size, + textStyle: config.labelStyle, + animationProgress: animationInfo.stateChangePercent, + color: config.lineStyle.color, + backgroundColor: chartTheme.backgroundColor, + ); + } + }, + drawEndPointLabel: () { + if (endPoint != null && + startPoint != null && + endPoint!.epoch != startPoint!.epoch) { + drawEpochLabel( + canvas: canvas, + epochToX: epochToX, + epoch: endPoint!.epoch, + size: size, + textStyle: config.labelStyle, + animationProgress: animationInfo.stateChangePercent, + color: config.lineStyle.color, + backgroundColor: chartTheme.backgroundColor, + ); + } + }, + ); } @override diff --git a/lib/src/deriv_chart/interactive_layer/interactive_layer.dart b/lib/src/deriv_chart/interactive_layer/interactive_layer.dart index 5774a207b..46a520691 100644 --- a/lib/src/deriv_chart/interactive_layer/interactive_layer.dart +++ b/lib/src/deriv_chart/interactive_layer/interactive_layer.dart @@ -13,6 +13,7 @@ import 'package:deriv_chart/src/deriv_chart/interactive_layer/crosshair/crosshai import 'package:deriv_chart/src/deriv_chart/interactive_layer/drawing_context.dart'; import 'package:deriv_chart/src/deriv_chart/interactive_layer/drawing_tool_gesture_recognizer.dart'; import 'package:deriv_chart/src/deriv_chart/interactive_layer/helpers/types.dart'; +import 'package:deriv_chart/src/deriv_chart/interactive_layer/interactive_layer_states/interactive_adding_tool_state.dart'; import 'package:deriv_chart/src/deriv_chart/interactive_layer/interactive_layer_states/interactive_selected_tool_state.dart'; import 'package:deriv_chart/src/models/axis_range.dart'; import 'package:deriv_chart/src/models/chart_config.dart'; @@ -789,16 +790,78 @@ class _InteractiveLayerGestureHandlerState layerConsumingHover ? InteractionMode.drawingTool : InteractionMode.none, ); - // For small screen variant, we don't show the crosshair on hover, as well as if we're in adding tool state - if (widget.crosshairVariant == CrosshairVariant.smallScreen || - layerConsumingHover) { - // InteractiveLayer is consuming the hover, we should not let the - // crosshair controller handle it + // Check if we should show crosshair based on drawing tool selection and hover state + final bool shouldShowCrosshair = + _shouldShowCrosshairOnHover(event.localPosition, layerConsumingHover); + + // For small screen variant, we don't show the crosshair on hover + if (widget.crosshairVariant == CrosshairVariant.smallScreen) { return; } - // Otherwise, let the crosshair controller handle the hover - widget.crosshairController.onHover(event); + // Show or hide crosshair based on the logic + if (shouldShowCrosshair) { + widget.crosshairController.onHover(event); + } else { + // Hide crosshair if it shouldn't be shown + widget.crosshairController.onExit(const PointerExitEvent()); + } + } + + /// Determines whether the crosshair should be shown based on drawing tool selection and hover state. + /// + /// The logic is: + /// - If a drawing tool is selected and the mouse is hovering over it, don't show crosshair + /// - If a drawing tool is not selected and the mouse hovers over it, show crosshair + /// - If no drawing tool is being hovered over, show crosshair (normal behavior) + bool _shouldShowCrosshairOnHover( + Offset localPosition, bool layerConsumingHover) { + final currentState = widget.interactiveLayerBehaviour.currentState; + + // If we're in adding tool state, don't show crosshair + if (currentState is InteractiveAddingToolState) { + return false; + } + + // If we're in selected tool state, only hide crosshair when hovering over the selected tool + if (currentState is InteractiveSelectedToolState) { + // Find which drawing we're hovering over + final InteractableDrawing? hoveredDrawing = + _findHoveredDrawing(localPosition); + + if (hoveredDrawing != null && + hoveredDrawing.id == currentState.selected.id) { + // Only hide crosshair if we're hovering over the selected tool + return false; + } + // For all other cases (hovering over different tool or empty space), show crosshair + return true; + } + + // For normal state, show crosshair + return true; + } + + /// Finds the drawing that is currently being hovered over. + InteractableDrawing? _findHoveredDrawing( + Offset localPosition) { + // Check regular drawings + for (final drawing in widget.drawings) { + if (drawing.hitTest(localPosition, epochToX, quoteToY)) { + return drawing; + } + } + + // Check preview drawings + for (final drawing in widget.interactiveLayerBehaviour.previewDrawings) { + if (drawing.hitTest(localPosition, epochToX, quoteToY)) { + // Preview drawings don't have the same interface, so we return null + // This is fine since preview drawings are temporary and shouldn't affect crosshair logic + return null; + } + } + + return null; } /// Determines the appropriate cursor based on the mouse position and interaction mode diff --git a/lib/src/models/chart_axis_config.dart b/lib/src/models/chart_axis_config.dart index b717a949c..5a23cb70a 100644 --- a/lib/src/models/chart_axis_config.dart +++ b/lib/src/models/chart_axis_config.dart @@ -1,4 +1,7 @@ import 'package:flutter/foundation.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'chart_axis_config.g.dart'; /// Default top bound quote. const double defaultTopBoundQuote = 60; @@ -12,6 +15,7 @@ const double defaultMaxCurrentTickOffset = 150; /// Configuration for the chart axis. @immutable +@JsonSerializable() class ChartAxisConfig { /// Initializes the chart axis configuration. const ChartAxisConfig({ @@ -25,6 +29,10 @@ class ChartAxisConfig { this.smoothScrolling = true, }); + /// Initializes from JSON. + factory ChartAxisConfig.fromJson(Map json) => + _$ChartAxisConfigFromJson(json); + /// Top quote bound target for animated transition. final double initialTopBoundQuote; @@ -60,6 +68,9 @@ class ChartAxisConfig { /// Default is `true`. final bool smoothScrolling; + /// Converts to JSON. + Map toJson() => _$ChartAxisConfigToJson(this); + /// Creates a copy of this ChartAxisConfig but with the given fields replaced. ChartAxisConfig copyWith({ double? initialTopBoundQuote, diff --git a/lib/src/models/chart_axis_config.g.dart b/lib/src/models/chart_axis_config.g.dart new file mode 100644 index 000000000..1d050cb61 --- /dev/null +++ b/lib/src/models/chart_axis_config.g.dart @@ -0,0 +1,38 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'chart_axis_config.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ChartAxisConfig _$ChartAxisConfigFromJson(Map json) => + ChartAxisConfig( + initialTopBoundQuote: + (json['initialTopBoundQuote'] as num?)?.toDouble() ?? + defaultTopBoundQuote, + initialBottomBoundQuote: + (json['initialBottomBoundQuote'] as num?)?.toDouble() ?? + defaultBottomBoundQuote, + maxCurrentTickOffset: + (json['maxCurrentTickOffset'] as num?)?.toDouble() ?? + defaultMaxCurrentTickOffset, + defaultIntervalWidth: + (json['defaultIntervalWidth'] as num?)?.toDouble() ?? 20, + showQuoteGrid: json['showQuoteGrid'] as bool? ?? true, + showEpochGrid: json['showEpochGrid'] as bool? ?? true, + showFrame: json['showFrame'] as bool? ?? false, + smoothScrolling: json['smoothScrolling'] as bool? ?? true, + ); + +Map _$ChartAxisConfigToJson(ChartAxisConfig instance) => + { + 'initialTopBoundQuote': instance.initialTopBoundQuote, + 'initialBottomBoundQuote': instance.initialBottomBoundQuote, + 'maxCurrentTickOffset': instance.maxCurrentTickOffset, + 'showQuoteGrid': instance.showQuoteGrid, + 'showEpochGrid': instance.showEpochGrid, + 'showFrame': instance.showFrame, + 'defaultIntervalWidth': instance.defaultIntervalWidth, + 'smoothScrolling': instance.smoothScrolling, + }; diff --git a/lib/src/models/chart_config.g.dart b/lib/src/models/chart_config.g.dart index 1c59a6904..732bea35b 100644 --- a/lib/src/models/chart_config.g.dart +++ b/lib/src/models/chart_config.g.dart @@ -7,12 +7,17 @@ part of 'chart_config.dart'; // ************************************************************************** ChartConfig _$ChartConfigFromJson(Map json) => ChartConfig( - granularity: json['granularity'] as int, - pipSize: json['pipSize'] as int? ?? 4, + granularity: (json['granularity'] as num).toInt(), + chartAxisConfig: json['chartAxisConfig'] == null + ? const ChartAxisConfig() + : ChartAxisConfig.fromJson( + json['chartAxisConfig'] as Map), + pipSize: (json['pipSize'] as num?)?.toInt() ?? 4, ); Map _$ChartConfigToJson(ChartConfig instance) => { 'pipSize': instance.pipSize, 'granularity': instance.granularity, + 'chartAxisConfig': instance.chartAxisConfig, }; diff --git a/lib/src/theme/painting_styles/overlay_style.dart b/lib/src/theme/painting_styles/overlay_style.dart index 38c6012c7..3fcc46dfe 100644 --- a/lib/src/theme/painting_styles/overlay_style.dart +++ b/lib/src/theme/painting_styles/overlay_style.dart @@ -1,7 +1,11 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'overlay_style.g.dart'; /// Style of the overlay. +@JsonSerializable() class OverlayStyle extends Equatable { /// Initializes a barrier style const OverlayStyle({ @@ -16,15 +20,27 @@ class OverlayStyle extends Equatable { ), }); + /// Initializes from JSON. + factory OverlayStyle.fromJson(Map json) => + _$OverlayStyleFromJson(json); + /// Height of the label. final double labelHeight; /// Color of the overlay barriers. + @JsonKey( + fromJson: _colorFromJson, + toJson: _colorToJson, + ) final Color color; /// Style of the text used in the overlay. + @JsonKey(includeFromJson: false, includeToJson: false) final TextStyle textStyle; + /// Converts to JSON. + Map toJson() => _$OverlayStyleToJson(this); + /// Creates a copy of this object. OverlayStyle copyWith({ double? labelHeight, @@ -43,3 +59,9 @@ class OverlayStyle extends Equatable { @override List get props => [labelHeight, color, textStyle]; } + +/// Converts a Color to JSON representation. +int _colorToJson(Color color) => color.value; + +/// Converts JSON representation to a Color. +Color _colorFromJson(int value) => Color(value); diff --git a/lib/src/theme/painting_styles/overlay_style.g.dart b/lib/src/theme/painting_styles/overlay_style.g.dart new file mode 100644 index 000000000..ba31de275 --- /dev/null +++ b/lib/src/theme/painting_styles/overlay_style.g.dart @@ -0,0 +1,20 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'overlay_style.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +OverlayStyle _$OverlayStyleFromJson(Map json) => OverlayStyle( + labelHeight: (json['labelHeight'] as num?)?.toDouble() ?? 24, + color: json['color'] == null + ? const Color(0xFF00A79E) + : _colorFromJson((json['color'] as num).toInt()), + ); + +Map _$OverlayStyleToJson(OverlayStyle instance) => + { + 'labelHeight': instance.labelHeight, + 'color': _colorToJson(instance.color), + }; diff --git a/showcase_app/lib/screens/chart_examples/base_chart_screen.dart b/showcase_app/lib/screens/chart_examples/base_chart_screen.dart index 26c2c2a9f..a3eebf253 100644 --- a/showcase_app/lib/screens/chart_examples/base_chart_screen.dart +++ b/showcase_app/lib/screens/chart_examples/base_chart_screen.dart @@ -20,6 +20,9 @@ abstract class BaseChartScreenState /// The chart candles. late List candles; + /// Whether the controls section is expanded. + bool _isControlsExpanded = true; + @override void initState() { super.initState(); @@ -36,13 +39,39 @@ abstract class BaseChartScreenState appBar: AppBar( title: Text(getTitle()), centerTitle: true, + actions: [ + IconButton( + icon: Icon( + _isControlsExpanded ? Icons.expand_less : Icons.expand_more, + ), + onPressed: () { + setState(() { + _isControlsExpanded = !_isControlsExpanded; + }); + }, + tooltip: _isControlsExpanded ? 'Hide Controls' : 'Show Controls', + ), + ], ), body: Column( children: [ Expanded( child: buildChart(), ), - buildControls(), + AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + height: _isControlsExpanded ? 300 : 0, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: _isControlsExpanded ? 1.0 : 0.0, + child: _isControlsExpanded + ? SingleChildScrollView( + child: buildControls(), + ) + : const SizedBox.shrink(), + ), + ), ], ), ); diff --git a/showcase_app/lib/screens/chart_examples/drawing_tools_screen.dart b/showcase_app/lib/screens/chart_examples/drawing_tools_screen.dart index aefcf42cf..76dc5433a 100644 --- a/showcase_app/lib/screens/chart_examples/drawing_tools_screen.dart +++ b/showcase_app/lib/screens/chart_examples/drawing_tools_screen.dart @@ -222,38 +222,38 @@ class _DrawingToolsScreenState DropdownButton( value: _selectedDrawingTool, hint: const Text('Select a drawing tool'), - items: const >[ - DropdownMenuItem( + items: >[ + const DropdownMenuItem( value: LineDrawingToolConfig(), - child: Text('Line'), + child: Text('Trend Line'), ), - DropdownMenuItem( + const DropdownMenuItem( value: HorizontalDrawingToolConfig(), child: Text('Horizontal'), ), - DropdownMenuItem( + const DropdownMenuItem( value: VerticalDrawingToolConfig(), child: Text('Vertical'), ), - DropdownMenuItem( + const DropdownMenuItem( value: RayDrawingToolConfig(), child: Text('Ray'), ), - DropdownMenuItem( + const DropdownMenuItem( value: TrendDrawingToolConfig(), child: Text('Trend'), ), - DropdownMenuItem( + const DropdownMenuItem( value: RectangleDrawingToolConfig(), child: Text('Rectangle'), ), - DropdownMenuItem( + const DropdownMenuItem( value: ChannelDrawingToolConfig(), child: Text('Channel'), ), DropdownMenuItem( value: FibfanDrawingToolConfig(), - child: Text('Fibonacci Fan'), + child: const Text('Fibonacci Fan'), ), ], onChanged: (DrawingToolConfig? config) {