1683 lines
77 KiB
MQL5
1683 lines
77 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| News Core.mqh |
| |||
//| Copyright 2026, Allan Munene Mutiiria. |
| |||
//| https://t.me/Forex_Algo_Trader |
| |||
//+------------------------------------------------------------------+
| |||
#property copyright "Copyright 2026, Allan Munene Mutiiria."
| |||
#property link "https://t.me/Forex_Algo_Trader"
| |||
| |||
//--- Include guard
| |||
#ifndef NEWS_CORE_MQH
| |||
#define NEWS_CORE_MQH
| |||
| |||
//--- Include canvas library
| |||
#include <Canvas/Canvas.mqh>
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Layout Constants |
| |||
//+------------------------------------------------------------------+
| |||
//--- Runtime dashboard width driven by g_news_dashW; bounds prevent unusable layouts
| |||
#define NEWS_DASHBOARD_W_DEFAULT 700 // Default starting width
| |||
#define NEWS_DASHBOARD_W_MIN 400 // Minimum allowed width
| |||
#define NEWS_DASHBOARD_W_MAX 1400 // Maximum allowed width
| |||
| |||
//--- Vertical bounds; flexible section is the events table
| |||
#define NEWS_DASHBOARD_H_DEFAULT 420 // Default starting height
| |||
#define NEWS_DASHBOARD_H_MIN 320 // Minimum height for at least 3 rows
| |||
#define NEWS_DASHBOARD_H_MAX 900 // Maximum allowed height
| |||
| |||
#define NEWS_HEADER_H 40 // Header strip height
| |||
#define NEWS_FILTER_H 36 // Filter toggle row height
| |||
#define NEWS_CURR_ROW_H 32 // Currency chip row height
| |||
#define NEWS_IMPACT_ROW_H 32 // Impact pill row height
| |||
#define NEWS_TABLE_HDR_H 28 // Table header row height
| |||
#define NEWS_ROW_H 26 // Event row height
| |||
#define NEWS_VISIBLE_ROWS 8 // Default visible row count (computed at runtime)
| |||
#define NEWS_SIDE_PAD 10 // Horizontal padding inside dashboard
| |||
#define NEWS_VERT_GAP 6 // Vertical gap between sections
| |||
#define NEWS_RESIZE_HOT_W 6 // Right-edge resize hot zone width
| |||
#define NEWS_RESIZE_HOT_H 6 // Bottom-edge resize hot zone height
| |||
#define NEWS_RESIZE_HANDLE_H 30 // Visible right-edge handle length (centered)
| |||
#define NEWS_RESIZE_HANDLE_W 30 // Visible bottom-edge handle length (centered)
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Runtime Dashboard Size and Resize State |
| |||
//+------------------------------------------------------------------+
| |||
int g_news_dashW = NEWS_DASHBOARD_W_DEFAULT; // Current dashboard width
| |||
int g_news_dashH = NEWS_DASHBOARD_H_DEFAULT; // Current dashboard height
| |||
bool g_news_resizing = false; // Right-edge drag active flag
| |||
int g_news_resizeStartMouseX = 0; // Mouse X at resize start
| |||
int g_news_resizeStartW = 0; // Dashboard width at resize start
| |||
bool g_news_resizingV = false; // Bottom-edge drag active flag
| |||
int g_news_resizeStartMouseY = 0; // Mouse Y at resize start
| |||
int g_news_resizeStartH = 0; // Dashboard height at resize start
| |||
| |||
//--- Backwards-compatibility aliases
| |||
#define NEWS_DASHBOARD_W g_news_dashW
| |||
#define NEWS_DASHBOARD_H g_news_dashH
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Table Column Reference Widths |
| |||
//+------------------------------------------------------------------+
| |||
//--- Reference widths at default dashboard width 700; all columns scale proportionally
| |||
const int NEWS_COL_W_REF[] = {68, 48, 42, 28, 215, 52, 60, 60, 70};
| |||
const string NEWS_COL_LABELS[] = {"Date", "Time", "Cur.", "Imp.", "Event", "Actual", "Forecast", "Previous", "Remain"};
| |||
#define NEWS_COL_COUNT 9
| |||
| |||
//--- Actual current column widths after proportional scaling; filled by News_ComputeColumnWidths
| |||
int g_news_colW[NEWS_COL_COUNT];
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Font Size Constants |
| |||
//+------------------------------------------------------------------+
| |||
#define NEWS_FONT_TITLE 12 // Title text size
| |||
#define NEWS_FONT_HEADING 10 // Heading text size
| |||
#define NEWS_FONT_BODY 9 // Body text size
| |||
#define NEWS_FONT_LABEL 9 // Label text size
| |||
#define NEWS_FONT_BUTTON 9 // Button text size
| |||
#define NEWS_FONT_TIMESTAMP 8 // Timestamp text size
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Glyph Codes |
| |||
//+------------------------------------------------------------------+
| |||
#define NEWS_GLYPH_CLOSE "r" // Webdings X close glyph
| |||
#define NEWS_GLYPH_CHECK "\x6FC" // Wingdings checkmark glyph
| |||
#define NEWS_GLYPH_CROSS "\x71B" // Wingdings X mark glyph
| |||
#define NEWS_GLYPH_DOT "l" // Wingdings filled dot glyph
| |||
#define NEWS_GLYPH_ARROW_UP "5" // Webdings up arrow glyph
| |||
#define NEWS_GLYPH_ARROW_DOWN "6" // Webdings down arrow glyph
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Canvas Object Names |
| |||
//+------------------------------------------------------------------+
| |||
#define NEWS_CANVAS_NAME "NewsCanvasMain" // Main canvas object name
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Theme Color Globals |
| |||
//+------------------------------------------------------------------+
| |||
color g_news_bg; // Dashboard background
| |||
color g_news_panelAlt; // Alternate panel color
| |||
color g_news_headerBg; // Header strip background
| |||
color g_news_border; // Subtle border color
| |||
color g_news_borderAccent; // Strong outer border color
| |||
color g_news_titleText; // Primary text color
| |||
color g_news_subText; // Secondary text color
| |||
color g_news_accent; // Accent blue color
| |||
| |||
//--- Filter chip colors
| |||
color g_news_chipOnBg; // Active chip background
| |||
color g_news_chipOnText; // Active chip text
| |||
color g_news_chipOffBg; // Inactive chip background
| |||
color g_news_chipOffText; // Inactive chip text
| |||
color g_news_chipHoverTint; // Hover overlay tint
| |||
| |||
//--- Currency chip colors
| |||
color g_news_currOnBg; // Selected currency background
| |||
color g_news_currOnText; // Selected currency text
| |||
color g_news_currOffBg; // Unselected currency background
| |||
color g_news_currOffText; // Unselected currency text
| |||
| |||
//--- Impact pill colors (semantic; same in both themes)
| |||
color g_news_impNone; // Impact none (gray)
| |||
color g_news_impLow; // Impact low (yellow)
| |||
color g_news_impMed; // Impact medium (orange)
| |||
color g_news_impHigh; // Impact high (red)
| |||
| |||
//--- Table colors
| |||
color g_news_tableHdrBg; // Table header strip background
| |||
color g_news_tableHdrText; // Table header text
| |||
color g_news_rowAlt; // Alternating row background
| |||
color g_news_rowText; // Row text color
| |||
color g_news_rowHover; // Row hover background
| |||
| |||
//--- Actual value direction colors
| |||
color g_news_actualUp; // Actual beat forecast (green)
| |||
color g_news_actualDown; // Actual missed forecast (red)
| |||
color g_news_revisedMark; // Revised previous indicator (gold)
| |||
color g_news_remainSoon; // Remain color when event is within 30 min
| |||
| |||
//--- Day separator row colors
| |||
color g_news_dayHeaderBg; // Day separator background
| |||
color g_news_dayHeaderText; // Day separator text
| |||
| |||
//--- Close button colors
| |||
color g_news_closeColor; // Close glyph idle color
| |||
color g_news_closeColorHover; // Close glyph hover color
| |||
color g_news_closeBgHover; // Close button hover background
| |||
| |||
//--- Toast colors
| |||
color g_news_toastBg; // Toast box background
| |||
color g_news_toastBorder; // Toast box border
| |||
color g_news_toastSuccess; // Toast success text
| |||
color g_news_toastError; // Toast error text
| |||
| |||
//--- Countdown banner colors
| |||
color g_news_countdownBg; // Active countdown background
| |||
color g_news_countdownReleaseBg; // Released countdown background
| |||
color g_news_countdownText; // Countdown text color
| |||
| |||
//--- Scrollbar thumb colors
| |||
color g_news_scrollSlider; // Idle thumb color
| |||
color g_news_scrollSliderHover; // Hover thumb color
| |||
color g_news_scrollSliderDrag; // Dragging thumb color
| |||
| |||
//--- Theme state
| |||
bool g_news_darkTheme = true; // Current theme flag
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Apply theme palette - dark or light |
| |||
//+------------------------------------------------------------------+
| |||
void News_ApplyTheme(bool dark)
| |||
{
| |||
//--- Store current theme flag
| |||
g_news_darkTheme = dark;
| |||
//--- Branch on theme mode
| |||
if(dark)
| |||
{
| |||
//--- Apply dark theme: charcoal with blue accent
| |||
g_news_bg = (color)C'24,26,32';
| |||
g_news_panelAlt = (color)C'30,33,40';
| |||
g_news_headerBg = (color)C'18,20,24';
| |||
g_news_border = (color)C'48,52,60';
| |||
g_news_borderAccent = (color)C'72,78,90';
| |||
g_news_titleText = (color)C'232,234,240';
| |||
g_news_subText = (color)C'148,152,162';
| |||
g_news_accent = (color)C'68,138,255';
| |||
g_news_chipOnBg = (color)C'68,138,255';
| |||
g_news_chipOnText = (color)C'255,255,255';
| |||
g_news_chipOffBg = (color)C'40,44,52';
| |||
g_news_chipOffText = (color)C'180,184,194';
| |||
g_news_chipHoverTint = (color)C'255,255,255';
| |||
g_news_currOnBg = (color)C'48,80,128';
| |||
g_news_currOnText = (color)C'255,255,255';
| |||
g_news_currOffBg = (color)C'36,40,48';
| |||
g_news_currOffText = (color)C'160,164,174';
| |||
g_news_impNone = (color)C'120,124,132';
| |||
g_news_impLow = (color)C'230,200,80';
| |||
g_news_impMed = (color)C'240,150,60';
| |||
g_news_impHigh = (color)C'225,80,80';
| |||
g_news_tableHdrBg = (color)C'36,40,48';
| |||
g_news_tableHdrText = (color)C'200,204,214';
| |||
g_news_rowAlt = (color)C'28,30,36';
| |||
g_news_rowText = (color)C'220,222,232';
| |||
g_news_rowHover = (color)C'70,76,92';
| |||
g_news_actualUp = (color)C'120,210,130';
| |||
g_news_actualDown = (color)C'235,100,100';
| |||
g_news_revisedMark = (color)C'250,200,60';
| |||
g_news_remainSoon = (color)C'255,90,90';
| |||
g_news_dayHeaderBg = (color)C'48,80,128';
| |||
g_news_dayHeaderText = (color)C'235,238,245';
| |||
g_news_closeColor = (color)C'180,184,194';
| |||
g_news_closeColorHover = (color)C'255,255,255';
| |||
g_news_closeBgHover = (color)C'180,60,60';
| |||
g_news_toastBg = (color)C'36,40,48';
| |||
g_news_toastBorder = (color)C'72,78,90';
| |||
g_news_toastSuccess = (color)C'120,200,120';
| |||
g_news_toastError = (color)C'225,100,100';
| |||
g_news_countdownBg = (color)C'48,80,128';
| |||
g_news_countdownReleaseBg = (color)C'140,60,60';
| |||
g_news_countdownText = (color)C'255,255,255';
| |||
g_news_scrollSlider = (color)C'90,100,120';
| |||
g_news_scrollSliderHover = (color)C'140,150,170';
| |||
g_news_scrollSliderDrag = (color)C'88,160,255';
| |||
}
| |||
else
| |||
{
| |||
//--- Apply light theme: white with blue accent
| |||
g_news_bg = (color)C'248,249,251';
| |||
g_news_panelAlt = (color)C'255,255,255';
| |||
g_news_headerBg = (color)C'238,240,244';
| |||
g_news_border = (color)C'218,222,228';
| |||
g_news_borderAccent = (color)C'180,186,196';
| |||
g_news_titleText = (color)C'24,28,36';
| |||
g_news_subText = (color)C'108,114,124';
| |||
g_news_accent = (color)C'40,110,220';
| |||
g_news_chipOnBg = (color)C'40,110,220';
| |||
g_news_chipOnText = (color)C'255,255,255';
| |||
g_news_chipOffBg = (color)C'232,236,242';
| |||
g_news_chipOffText = (color)C'70,76,86';
| |||
g_news_chipHoverTint = (color)C'0,0,0';
| |||
g_news_currOnBg = (color)C'200,220,250';
| |||
g_news_currOnText = (color)C'30,60,140';
| |||
g_news_currOffBg = (color)C'238,240,244';
| |||
g_news_currOffText = (color)C'90,96,106';
| |||
g_news_impNone = (color)C'140,144,152';
| |||
g_news_impLow = (color)C'220,180,40';
| |||
g_news_impMed = (color)C'230,130,40';
| |||
g_news_impHigh = (color)C'210,60,60';
| |||
g_news_tableHdrBg = (color)C'232,236,242';
| |||
g_news_tableHdrText = (color)C'40,46,56';
| |||
g_news_rowAlt = (color)C'248,250,253';
| |||
g_news_rowText = (color)C'30,34,42';
| |||
g_news_rowHover = (color)C'232,238,248';
| |||
g_news_actualUp = (color)C'30,140,60';
| |||
g_news_actualDown = (color)C'200,40,40';
| |||
g_news_revisedMark = (color)C'200,110,15';
| |||
g_news_remainSoon = (color)C'205,40,40';
| |||
g_news_dayHeaderBg = (color)C'215,228,245';
| |||
g_news_dayHeaderText = (color)C'30,60,140';
| |||
g_news_closeColor = (color)C'120,126,136';
| |||
g_news_closeColorHover = (color)C'255,255,255';
| |||
g_news_closeBgHover = (color)C'200,60,60';
| |||
g_news_toastBg = (color)C'255,255,255';
| |||
g_news_toastBorder = (color)C'180,186,196';
| |||
g_news_toastSuccess = (color)C'40,140,60';
| |||
g_news_toastError = (color)C'200,60,60';
| |||
g_news_countdownBg = (color)C'40,110,220';
| |||
g_news_countdownReleaseBg = (color)C'200,80,80';
| |||
g_news_countdownText = (color)C'255,255,255';
| |||
g_news_scrollSlider = (color)C'170,178,190';
| |||
g_news_scrollSliderHover = (color)C'120,128,142';
| |||
g_news_scrollSliderDrag = (color)C'40,110,220';
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Fast canvas subclass exposing direct pixel operations |
| |||
//+------------------------------------------------------------------+
| |||
class CNewsCanvasFast : public CCanvas
| |||
{
| |||
public:
| |||
int PixelWidth() { return m_width; } // Return canvas pixel width
| |||
int PixelHeight() { return m_height; } // Return canvas pixel height
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Copy rect from another canvas into this one at same coords |
| |||
//+------------------------------------------------------------------+
| |||
void CopyRectFromCanvas(CCanvas &src, int l, int t, int r, int b)
| |||
{
| |||
//--- Compute source and destination dimensions
| |||
const int sw = src.Width();
| |||
const int sh = src.Height();
| |||
const int dw = Width();
| |||
const int dh = Height();
| |||
//--- Clamp copy bounds to valid region
| |||
const int cl = MathMax(0, l);
| |||
const int ct = MathMax(0, t);
| |||
const int cr = MathMin(MathMin(r, sw), dw);
| |||
const int cb = MathMin(MathMin(b, sh), dh);
| |||
//--- Copy each pixel row by row
| |||
for(int yy = ct; yy < cb; yy++)
| |||
for(int xx = cl; xx < cr; xx++)
| |||
m_pixels[yy * dw + xx] = src.PixelGet(xx, yy);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Copy rect from this canvas to a destination canvas at same coords|
| |||
//+------------------------------------------------------------------+
| |||
void CopyRectToCanvas(CCanvas &dst, int l, int t, int r, int b)
| |||
{
| |||
//--- Compute destination and source dimensions
| |||
const int dwOther = dst.Width();
| |||
const int dhOther = dst.Height();
| |||
const int sw = Width();
| |||
const int sh = Height();
| |||
//--- Clamp copy bounds to valid region
| |||
const int cl = MathMax(0, l);
| |||
const int ct = MathMax(0, t);
| |||
const int cr = MathMin(MathMin(r, sw), dwOther);
| |||
const int cb = MathMin(MathMin(b, sh), dhOther);
| |||
//--- Copy each pixel row by row
| |||
for(int yy = ct; yy < cb; yy++)
| |||
for(int xx = cl; xx < cr; xx++)
| |||
dst.PixelSet(xx, yy, m_pixels[yy * sw + xx]);
| |||
}
| |||
};
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Lighten a color by amount (0..1) |
| |||
//+------------------------------------------------------------------+
| |||
color News_LightenColor(color c, double amount)
| |||
{
| |||
//--- Decompose into RGB channels
| |||
const int r = (c & 0xFF);
| |||
const int g = (c >> 8) & 0xFF;
| |||
const int b = (c >> 16) & 0xFF;
| |||
//--- Blend each channel toward white
| |||
const int nr = (int)MathMin(255, r + (255 - r) * amount);
| |||
const int ng = (int)MathMin(255, g + (255 - g) * amount);
| |||
const int nb = (int)MathMin(255, b + (255 - b) * amount);
| |||
//--- Reassemble and return lightened color
| |||
return (color)((nb << 16) | (ng << 8) | nr);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Darken a color by amount (0..1) |
| |||
//+------------------------------------------------------------------+
| |||
color News_DarkenColor(color c, double amount)
| |||
{
| |||
//--- Decompose into RGB channels
| |||
const int r = (c & 0xFF);
| |||
const int g = (c >> 8) & 0xFF;
| |||
const int b = (c >> 16) & 0xFF;
| |||
//--- Multiply each channel toward black
| |||
const int nr = (int)MathMax(0, r * amount);
| |||
const int ng = (int)MathMax(0, g * amount);
| |||
const int nb = (int)MathMax(0, b * amount);
| |||
//--- Reassemble and return darkened color
| |||
return (color)((nb << 16) | (ng << 8) | nr);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Compute theme-aware border color for given fill |
| |||
//+------------------------------------------------------------------+
| |||
color News_BorderForBg(color bg)
| |||
{
| |||
//--- Lighten in dark theme, darken in light theme
| |||
if(g_news_darkTheme) return News_LightenColor(bg, 0.3);
| |||
return News_DarkenColor(bg, 0.78);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Compute theme-aware hover color for a given base background |
| |||
//+------------------------------------------------------------------+
| |||
color News_HoverForBg(color bg)
| |||
{
| |||
//--- Lighten dark backgrounds, darken light backgrounds on hover
| |||
if(g_news_darkTheme) return News_LightenColor(bg, 0.20);
| |||
return News_DarkenColor(bg, 0.90);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Compute current column widths from runtime dashboard width |
| |||
//+------------------------------------------------------------------+
| |||
void News_ComputeColumnWidths()
| |||
{
| |||
//--- Compute proportional scale factor relative to default width
| |||
const double scale = (double)g_news_dashW / (double)NEWS_DASHBOARD_W_DEFAULT;
| |||
//--- Scale each reference column width and enforce 20px floor
| |||
for(int i = 0; i < NEWS_COL_COUNT; i++)
| |||
{
| |||
int w = (int)MathRound(NEWS_COL_W_REF[i] * scale);
| |||
if(w < 20) w = 20;
| |||
g_news_colW[i] = w;
| |||
}
| |||
//--- Compute layout geometry to determine scrollbar reservation
| |||
const int sidePad = 10;
| |||
const int xStart = 6;
| |||
const int totalRows = ArraySize(g_news_rowPlan);
| |||
const int rowsTop = NEWS_HEADER_H + 36 + 32 + 32 + 6 + 28;
| |||
const int footerTop = g_news_dashH - 26 - 8;
| |||
const int viewportH = footerTop - rowsTop;
| |||
const int contentH = totalRows * 26;
| |||
const bool scrollNeeded = (contentH > viewportH);
| |||
const int scrollReserve = scrollNeeded ? 12 : 0;
| |||
const int availW = g_news_dashW - 2 * sidePad - xStart - scrollReserve;
| |||
//--- Sum all scaled column widths
| |||
int sumW = 0;
| |||
for(int i = 0; i < NEWS_COL_COUNT; i++) sumW += g_news_colW[i];
| |||
//--- Shrink the Event column (index 4) if total exceeds available width
| |||
if(sumW > availW)
| |||
{
| |||
const int over = sumW - availW;
| |||
int eventW = g_news_colW[4] - over;
| |||
if(eventW < 80) eventW = 80;
| |||
g_news_colW[4] = eventW;
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Blend a single ARGB pixel onto canvas at (x, y) |
| |||
//+------------------------------------------------------------------+
| |||
void News_BlendPixel(CCanvas &canv, int x, int y, uint argb)
| |||
{
| |||
//--- Skip pixels outside canvas bounds
| |||
if(x < 0 || y < 0 || x >= canv.Width() || y >= canv.Height()) return;
| |||
//--- Extract source alpha; skip fully transparent pixels
| |||
const uchar sa = (uchar)((argb >> 24) & 0xFF);
| |||
if(sa == 0) return;
| |||
//--- Extract source RGB channels
| |||
const uchar sr = (uchar)((argb >> 16) & 0xFF);
| |||
const uchar sg = (uchar)((argb >> 8) & 0xFF);
| |||
const uchar sb = (uchar)(argb & 0xFF);
| |||
//--- Read destination pixel and extract its channels
| |||
const uint dst = canv.PixelGet(x, y);
| |||
const uchar da = (uchar)((dst >> 24) & 0xFF);
| |||
const uchar dr = (uchar)((dst >> 16) & 0xFF);
| |||
const uchar dg = (uchar)((dst >> 8) & 0xFF);
| |||
const uchar db = (uchar)(dst & 0xFF);
| |||
//--- Perform source-over alpha composite
| |||
const int isa = 255 - sa;
| |||
const uchar or_ = (uchar)((sa * sr + isa * dr) / 255);
| |||
const uchar og = (uchar)((sa * sg + isa * dg) / 255);
| |||
const uchar ob = (uchar)((sa * sb + isa * db) / 255);
| |||
const uchar oa = (uchar)(sa + (isa * da) / 255);
| |||
canv.PixelSet(x, y, (oa << 24) | (or_ << 16) | (og << 8) | ob);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get text width in pixels |
| |||
//+------------------------------------------------------------------+
| |||
int News_TextWidth(string text, string font, int size)
| |||
{
| |||
//--- Allocate a shared single-pixel measurement canvas on first call
| |||
static CCanvas s_measureCanvas;
| |||
static bool s_ready = false;
| |||
if(!s_ready)
| |||
{
| |||
s_measureCanvas.CreateBitmap("NewsMeasureCanvas", 0, 0, 1, 1, COLOR_FORMAT_ARGB_NORMALIZE);
| |||
s_ready = true;
| |||
}
| |||
//--- Set font and measure text dimensions
| |||
s_measureCanvas.FontSet(font, -size * 10, FW_NORMAL);
| |||
int w = 0, h = 0;
| |||
s_measureCanvas.TextSize(text, w, h);
| |||
return w;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get text height in pixels |
| |||
//+------------------------------------------------------------------+
| |||
int News_TextHeight(string font, int size)
| |||
{
| |||
//--- Allocate a shared single-pixel measurement canvas on first call
| |||
static CCanvas s_measureCanvas;
| |||
static bool s_ready = false;
| |||
if(!s_ready)
| |||
{
| |||
s_measureCanvas.CreateBitmap("NewsMeasureCanvas2", 0, 0, 1, 1, COLOR_FORMAT_ARGB_NORMALIZE);
| |||
s_ready = true;
| |||
}
| |||
//--- Measure height using a capital-descender reference string
| |||
s_measureCanvas.FontSet(font, -size * 10, FW_NORMAL);
| |||
int w = 0, h = 0;
| |||
s_measureCanvas.TextSize("Mg", w, h);
| |||
return h;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Stamp normal-weight text onto canvas |
| |||
//+------------------------------------------------------------------+
| |||
void News_StampText(CCanvas &canv, int x, int y, string text, string font, int size, color clr)
| |||
{
| |||
//--- Set normal font weight and draw text at position
| |||
canv.FontSet(font, -size * 10, FW_NORMAL);
| |||
canv.TextOut(x, y, text, ColorToARGB(clr, 255), TA_LEFT | TA_TOP);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Stamp bold text onto canvas |
| |||
//+------------------------------------------------------------------+
| |||
void News_StampTextBold(CCanvas &canv, int x, int y, string text, string font, int size, color clr)
| |||
{
| |||
//--- Set bold font weight and draw text at position
| |||
canv.FontSet(font, -size * 10, FW_BOLD);
| |||
canv.TextOut(x, y, text, ColorToARGB(clr, 255), TA_LEFT | TA_TOP);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Fit text into width with ellipsis fallback |
| |||
//+------------------------------------------------------------------+
| |||
string News_FitTextToWidth(string text, string font, int size, int maxW)
| |||
{
| |||
//--- Return text unchanged if it already fits
| |||
if(News_TextWidth(text, font, size) <= maxW) return text;
| |||
//--- Measure ellipsis width; return empty if even ellipsis cannot fit
| |||
const string ellipsis = "...";
| |||
const int ellW = News_TextWidth(ellipsis, font, size);
| |||
if(ellW >= maxW) return "";
| |||
//--- Binary search for the longest prefix that fits with ellipsis appended
| |||
const int textLen = StringLen(text);
| |||
int lo = 0, hi = textLen, fit = 0;
| |||
while(lo <= hi)
| |||
{
| |||
const int mid = (lo + hi) / 2;
| |||
const string pre = StringSubstr(text, 0, mid) + ellipsis;
| |||
if(News_TextWidth(pre, font, size) <= maxW) { fit = mid; lo = mid + 1; }
| |||
else { hi = mid - 1; }
| |||
}
| |||
return StringSubstr(text, 0, fit) + ellipsis;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Drawing primitives helper - supersampled HR rendering |
| |||
//+------------------------------------------------------------------+
| |||
class CNewsPrimitives
| |||
{
| |||
public:
| |||
CNewsCanvasFast m_hrFill; // HR fill offscreen canvas
| |||
bool m_hrFillReady; // HR fill canvas ready flag
| |||
int m_hrFillW; // HR fill canvas width
| |||
int m_hrFillH; // HR fill canvas height
| |||
CNewsCanvasFast m_hrBorder; // HR border offscreen canvas
| |||
bool m_hrBorderReady; // HR border canvas ready flag
| |||
int m_hrBorderW; // HR border canvas width
| |||
int m_hrBorderH; // HR border canvas height
| |||
| |||
//--- Constructor: initialize all flags and sizes to zero/false
| |||
CNewsPrimitives()
| |||
{
| |||
m_hrFillReady = false; m_hrFillW = 0; m_hrFillH = 0;
| |||
m_hrBorderReady = false; m_hrBorderW = 0; m_hrBorderH = 0;
| |||
}
| |||
| |||
bool EnsureHrFill(int needW, int needH);
| |||
bool EnsureHrBorder(int needW, int needH);
| |||
void BlendPixelSet(CCanvas &canvas, int x, int y, uint src);
| |||
void FillCornerQuadrantHR(CCanvas &canvas, int cx, int cy, int radius, uint argb, int signX, int signY);
| |||
void FillRoundRectHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb);
| |||
void DrawRoundRectBorderHR(CCanvas &canvas, int x, int y, int w, int h, int radius, int thickness, uint argb);
| |||
void FillRoundRectSharp(CCanvas &target, int x, int y, int w, int h, int radius, uint argb, int factor = 4);
| |||
void DrawRoundRectBorderSharp(CCanvas &target, int x, int y, int w, int h, int radius, int thickness, uint argb, int factor = 4);
| |||
void DrawRoundRectBorderObStyle(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb);
| |||
};
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Ensure HR fill canvas is allocated and large enough |
| |||
//+------------------------------------------------------------------+
| |||
bool CNewsPrimitives::EnsureHrFill(int needW, int needH)
| |||
{
| |||
//--- Return early if canvas already meets size requirements
| |||
if(m_hrFillReady && needW <= m_hrFillW && needH <= m_hrFillH) return true;
| |||
//--- Grow to the larger of current and requested dimensions
| |||
const int w = MathMax(needW, m_hrFillW);
| |||
const int h = MathMax(needH, m_hrFillH);
| |||
//--- Destroy stale canvas before recreating
| |||
if(m_hrFillReady) { m_hrFill.Destroy(); m_hrFillReady = false; }
| |||
if(!m_hrFill.CreateBitmap("NewsPrimHrFill", 0, 0, w, h, COLOR_FORMAT_ARGB_NORMALIZE)) return false;
| |||
//--- Record new dimensions and mark ready
| |||
m_hrFillW = w; m_hrFillH = h;
| |||
m_hrFillReady = true;
| |||
return true;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Ensure HR border canvas is allocated and large enough |
| |||
//+------------------------------------------------------------------+
| |||
bool CNewsPrimitives::EnsureHrBorder(int needW, int needH)
| |||
{
| |||
//--- Return early if canvas already meets size requirements
| |||
if(m_hrBorderReady && needW <= m_hrBorderW && needH <= m_hrBorderH) return true;
| |||
//--- Grow to the larger of current and requested dimensions
| |||
const int w = MathMax(needW, m_hrBorderW);
| |||
const int h = MathMax(needH, m_hrBorderH);
| |||
//--- Destroy stale canvas before recreating
| |||
if(m_hrBorderReady) { m_hrBorder.Destroy(); m_hrBorderReady = false; }
| |||
if(!m_hrBorder.CreateBitmap("NewsPrimHrBorder", 0, 0, w, h, COLOR_FORMAT_ARGB_NORMALIZE)) return false;
| |||
//--- Record new dimensions and mark ready
| |||
m_hrBorderW = w; m_hrBorderH = h;
| |||
m_hrBorderReady = true;
| |||
return true;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Blend a single pixel onto canvas using src-over alpha |
| |||
//+------------------------------------------------------------------+
| |||
void CNewsPrimitives::BlendPixelSet(CCanvas &canvas, int x, int y, uint src)
| |||
{
| |||
//--- Bounds check; skip out-of-range pixels
| |||
if(x < 0 || x >= canvas.Width() || y < 0 || y >= canvas.Height()) return;
| |||
//--- Decompose source and destination into float channels
| |||
uint dst = canvas.PixelGet(x, y);
| |||
double sA = ((src >> 24) & 0xFF) / 255.0, sR = ((src >> 16) & 0xFF) / 255.0;
| |||
double sG = ((src >> 8) & 0xFF) / 255.0, sB = ( src & 0xFF) / 255.0;
| |||
double dA = ((dst >> 24) & 0xFF) / 255.0, dR = ((dst >> 16) & 0xFF) / 255.0;
| |||
double dG = ((dst >> 8) & 0xFF) / 255.0, dB = ( dst & 0xFF) / 255.0;
| |||
//--- Compute output alpha via src-over formula
| |||
double oA = sA + dA * (1.0 - sA);
| |||
if(oA == 0.0) { canvas.PixelSet(x, y, 0); return; }
| |||
//--- Composite and write final pixel
| |||
canvas.PixelSet(x, y,
| |||
((uint)(uchar)(oA * 255 + 0.5) << 24) |
| |||
((uint)(uchar)((sR * sA + dR * dA * (1.0 - sA)) / oA * 255 + 0.5) << 16) |
| |||
((uint)(uchar)((sG * sA + dG * dA * (1.0 - sA)) / oA * 255 + 0.5) << 8) |
| |||
(uint)(uchar)((sB * sA + dB * dA * (1.0 - sA)) / oA * 255 + 0.5));
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Fill quarter-circle corner on high-resolution canvas |
| |||
//+------------------------------------------------------------------+
| |||
void CNewsPrimitives::FillCornerQuadrantHR(CCanvas &canvas, int cx, int cy, int radius, uint argb, int signX, int signY)
| |||
{
| |||
//--- Set up radius, alpha, RGB, and subpixel grid
| |||
double rd = (double)radius;
| |||
uchar bA = (uchar)((argb >> 24) & 0xFF);
| |||
uint rgb = argb & 0x00FFFFFF;
| |||
int sub = 4;
| |||
double step = 1.0 / sub;
| |||
int subSq = sub * sub;
| |||
//--- Iterate over bounding box pixels
| |||
for(int dy = -(radius + 1); dy <= (radius + 1); dy++)
| |||
for(int dx = -(radius + 1); dx <= (radius + 1); dx++)
| |||
{
| |||
//--- Skip pixels outside this quadrant
| |||
bool inQ = ((signX > 0) ? (dx >= 0) : (dx <= 0)) && ((signY > 0) ? (dy >= 0) : (dy <= 0));
| |||
if(!inQ) continue;
| |||
//--- Skip pixels clearly outside or inside the arc band
| |||
double dist = MathSqrt((double)(dx * dx + dy * dy));
| |||
if(dist > rd + 1.0) continue;
| |||
if(dist <= rd - 1.0) { canvas.PixelSet(cx + dx, cy + dy, argb); continue; }
| |||
//--- Supersample the arc edge for anti-aliasing
| |||
int inside = 0;
| |||
for(int sy = 0; sy < sub; sy++)
| |||
for(int sx = 0; sx < sub; sx++)
| |||
{
| |||
double sdx = (double)dx - 0.5 + (sx + 0.5) * step;
| |||
double sdy = (double)dy - 0.5 + (sy + 0.5) * step;
| |||
if(sdx * sdx + sdy * sdy <= rd * rd) inside++;
| |||
}
| |||
if(inside == 0) continue;
| |||
//--- Blend partial coverage pixel
| |||
BlendPixelSet(canvas, cx + dx, cy + dy, (((uint)(uchar)((int)bA * inside / subSq)) << 24) | rgb);
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Fill rounded rectangle on HR canvas |
| |||
//+------------------------------------------------------------------+
| |||
void CNewsPrimitives::FillRoundRectHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb)
| |||
{
| |||
//--- Clamp radius to half the shortest dimension
| |||
radius = MathMin(radius, MathMin(w / 2, h / 2));
| |||
if(radius <= 0) { canvas.FillRectangle(x, y, x + w - 1, y + h - 1, argb); return; }
| |||
//--- Fill the three non-corner rectangles
| |||
canvas.FillRectangle(x + radius, y, x + w - radius - 1, y + h - 1, argb);
| |||
canvas.FillRectangle(x, y + radius, x + radius - 1, y + h - radius - 1, argb);
| |||
canvas.FillRectangle(x + w - radius, y + radius, x + w - 1, y + h - radius - 1, argb);
| |||
//--- Fill each anti-aliased corner quadrant
| |||
FillCornerQuadrantHR(canvas, x + radius, y + radius, radius, argb, -1, -1);
| |||
FillCornerQuadrantHR(canvas, x + w - radius, y + radius, radius, argb, 1, -1);
| |||
FillCornerQuadrantHR(canvas, x + radius, y + h - radius, radius, argb, -1, 1);
| |||
FillCornerQuadrantHR(canvas, x + w - radius, y + h - radius, radius, argb, 1, 1);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Draw rounded border on HR canvas (filled outer minus inner) |
| |||
//+------------------------------------------------------------------+
| |||
void CNewsPrimitives::DrawRoundRectBorderHR(CCanvas &canvas, int x, int y, int w, int h, int radius, int thickness, uint argb)
| |||
{
| |||
//--- Fill the outer rounded rect
| |||
FillRoundRectHR(canvas, x, y, w, h, radius, argb);
| |||
//--- Compute inner rect dimensions
| |||
const int innerX = x + thickness;
| |||
const int innerY = y + thickness;
| |||
const int innerW = w - 2 * thickness;
| |||
const int innerH = h - 2 * thickness;
| |||
const int innerR = MathMax(0, radius - thickness);
| |||
//--- Punch out inner area with transparent fill
| |||
if(innerW > 0 && innerH > 0)
| |||
FillRoundRectHR(canvas, innerX, innerY, innerW, innerH, innerR, 0x00000000);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Fill rounded rect with supersampled anti-aliasing |
| |||
//+------------------------------------------------------------------+
| |||
void CNewsPrimitives::FillRoundRectSharp(CCanvas &target, int x, int y, int w, int h, int radius, uint argb, int factor)
| |||
{
| |||
//--- Skip degenerate rectangles
| |||
if(w <= 0 || h <= 0) return;
| |||
//--- Allocate or grow HR offscreen canvas
| |||
const int hrW = w * factor, hrH = h * factor;
| |||
if(!EnsureHrFill(hrW, hrH)) return;
| |||
//--- Clear and fill HR canvas
| |||
m_hrFill.FillRectangle(0, 0, hrW - 1, hrH - 1, 0);
| |||
FillRoundRectHR(m_hrFill, 0, 0, hrW, hrH, radius * factor, argb);
| |||
//--- Downsample HR canvas into target
| |||
const int ss2 = factor * factor;
| |||
const int tW = target.Width(), tH = target.Height();
| |||
for(int py = 0; py < h; py++)
| |||
{
| |||
const int ty = y + py;
| |||
if(ty < 0 || ty >= tH) continue;
| |||
for(int px = 0; px < w; px++)
| |||
{
| |||
const int tx = x + px;
| |||
if(tx < 0 || tx >= tW) continue;
| |||
//--- Accumulate subpixel coverage
| |||
double sA = 0, sR = 0, sG = 0, sB = 0, wc = 0;
| |||
for(int dy = 0; dy < factor; dy++)
| |||
for(int dx = 0; dx < factor; dx++)
| |||
{
| |||
const int sx = px * factor + dx, sy = py * factor + dy;
| |||
if(sx >= hrW || sy >= hrH) continue;
| |||
const uint p = m_hrFill.PixelGet(sx, sy);
| |||
const uchar a = (uchar)((p >> 24) & 0xFF);
| |||
sA += a;
| |||
if(a > 0) { sR += (p >> 16) & 0xFF; sG += (p >> 8) & 0xFF; sB += p & 0xFF; wc += 1.0; }
| |||
}
| |||
//--- Blend averaged sample into target
| |||
const uchar fa = (uchar)(sA / ss2);
| |||
if(fa == 0 || wc == 0) continue;
| |||
const uint sample = ((uint)fa << 24) | ((uint)(uchar)(sR / wc) << 16) |
| |||
((uint)(uchar)(sG / wc) << 8) | (uint)(uchar)(sB / wc);
| |||
BlendPixelSet(target, tx, ty, sample);
| |||
}
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Draw rounded border with supersampled anti-aliasing |
| |||
//+------------------------------------------------------------------+
| |||
void CNewsPrimitives::DrawRoundRectBorderSharp(CCanvas &target, int x, int y, int w, int h, int radius, int thickness, uint argb, int factor)
| |||
{
| |||
//--- Skip degenerate or zero-thickness requests
| |||
if(w <= 0 || h <= 0 || thickness <= 0) return;
| |||
//--- Allocate or grow HR border offscreen canvas
| |||
const int hrW = w * factor, hrH = h * factor;
| |||
if(!EnsureHrBorder(hrW, hrH)) return;
| |||
//--- Clear and render border onto HR canvas
| |||
m_hrBorder.FillRectangle(0, 0, hrW - 1, hrH - 1, 0);
| |||
DrawRoundRectBorderHR(m_hrBorder, 0, 0, hrW, hrH, radius * factor, thickness * factor, argb);
| |||
//--- Downsample HR border canvas into target
| |||
const int ss2 = factor * factor;
| |||
const int tW = target.Width(), tH = target.Height();
| |||
for(int py = 0; py < h; py++)
| |||
{
| |||
const int ty = y + py;
| |||
if(ty < 0 || ty >= tH) continue;
| |||
for(int px = 0; px < w; px++)
| |||
{
| |||
const int tx = x + px;
| |||
if(tx < 0 || tx >= tW) continue;
| |||
//--- Accumulate subpixel coverage
| |||
double sA = 0, sR = 0, sG = 0, sB = 0, wc = 0;
| |||
for(int dy = 0; dy < factor; dy++)
| |||
for(int dx = 0; dx < factor; dx++)
| |||
{
| |||
const int sx = px * factor + dx, sy = py * factor + dy;
| |||
if(sx >= hrW || sy >= hrH) continue;
| |||
const uint p = m_hrBorder.PixelGet(sx, sy);
| |||
const uchar a = (uchar)((p >> 24) & 0xFF);
| |||
sA += a;
| |||
if(a > 0) { sR += (p >> 16) & 0xFF; sG += (p >> 8) & 0xFF; sB += p & 0xFF; wc += 1.0; }
| |||
}
| |||
//--- Blend averaged sample into target
| |||
const uchar fa = (uchar)(sA / ss2);
| |||
if(fa == 0 || wc == 0) continue;
| |||
const uint sample = ((uint)fa << 24) | ((uint)(uchar)(sR / wc) << 16) |
| |||
((uint)(uchar)(sG / wc) << 8) | (uint)(uchar)(sB / wc);
| |||
BlendPixelSet(target, tx, ty, sample);
| |||
}
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Global Primitives Instance |
| |||
//+------------------------------------------------------------------+
| |||
CNewsPrimitives g_news_prim; // Shared primitives helper instance
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Draw rounded rect border using arc-sampled 1px outline |
| |||
//+------------------------------------------------------------------+
| |||
void CNewsPrimitives::DrawRoundRectBorderObStyle(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb)
| |||
{
| |||
//--- Clamp radius and extract alpha and RGB
| |||
radius = MathMin(radius, MathMin(w / 2, h / 2));
| |||
if(radius < 0) radius = 0;
| |||
uchar bA = (uchar)((argb >> 24) & 0xFF);
| |||
uint rgb = argb & 0x00FFFFFF;
| |||
const int sub = 4;
| |||
const double step = 1.0 / sub;
| |||
const int subSq = sub * sub;
| |||
const double rd = (double)radius;
| |||
//--- Draw straight edges between corner arcs
| |||
canvas.Line(x + radius, y, x + w - radius - 1, y, argb);
| |||
canvas.Line(x + radius, y + h - 1, x + w - radius - 1, y + h - 1, argb);
| |||
canvas.Line(x, y + radius, x, y + h - radius - 1, argb);
| |||
canvas.Line(x + w - 1, y + radius, x + w - 1, y + h - radius - 1, argb);
| |||
if(radius == 0) return;
| |||
//--- Render anti-aliased arc for each of the four corners
| |||
for(int corner = 0; corner < 4; corner++)
| |||
{
| |||
int cx = (corner == 0 || corner == 2) ? (x + radius) : (x + w - 1 - radius);
| |||
int cy = (corner == 0 || corner == 1) ? (y + radius) : (y + h - 1 - radius);
| |||
int signX = (corner == 0 || corner == 2) ? -1 : 1;
| |||
int signY = (corner == 0 || corner == 1) ? -1 : 1;
| |||
for(int adyL = 0; adyL <= radius + 1; adyL++)
| |||
{
| |||
for(int adxL = 0; adxL <= radius + 1; adxL++)
| |||
{
| |||
//--- Skip pixels clearly outside the arc band
| |||
double dist = MathSqrt((double)(adxL * adxL + adyL * adyL));
| |||
if(dist > rd + 1.0 || dist < rd - 1.0) continue;
| |||
//--- Supersample the arc edge pixel
| |||
int inside = 0;
| |||
for(int sy2 = 0; sy2 < sub; sy2++)
| |||
for(int sx2 = 0; sx2 < sub; sx2++)
| |||
{
| |||
double sdx = (double)adxL - 0.5 + (sx2 + 0.5) * step;
| |||
double sdy = (double)adyL - 0.5 + (sy2 + 0.5) * step;
| |||
double sd = MathSqrt(sdx * sdx + sdy * sdy);
| |||
if(sd >= rd - 0.5 && sd <= rd + 0.5) inside++;
| |||
}
| |||
if(inside == 0) continue;
| |||
//--- Blend partial-coverage arc pixel
| |||
const uint sample = (((uint)(uchar)((int)bA * inside / subSq)) << 24) | rgb;
| |||
const int px = cx + adxL * signX;
| |||
const int py = cy + adyL * signY;
| |||
BlendPixelSet(canvas, px, py, sample);
| |||
}
| |||
}
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Fill rounded rect via supersampled HR rendering |
| |||
//+------------------------------------------------------------------+
| |||
void News_FillRoundRect(CCanvas &canv, int x, int y, int w, int h, int r, uint argb)
| |||
{
| |||
//--- Delegate to shared primitives instance
| |||
g_news_prim.FillRoundRectSharp(canv, x, y, w, h, r, argb, 4);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Draw rounded rect border via supersampled HR rendering |
| |||
//+------------------------------------------------------------------+
| |||
void News_DrawRoundRectBorder(CCanvas &canv, int x, int y, int w, int h, int r, int thick, uint argb)
| |||
{
| |||
//--- Delegate to shared primitives instance
| |||
g_news_prim.DrawRoundRectBorderSharp(canv, x, y, w, h, r, thick, argb, 4);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Test if point lies inside a rectangle |
| |||
//+------------------------------------------------------------------+
| |||
bool News_PointInRect(int px, int py, int rx, int ry, int rw, int rh)
| |||
{
| |||
//--- Return true when point is within bounds
| |||
return (px >= rx && px < rx + rw && py >= ry && py < ry + rh);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Draw anti-aliased thick line |
| |||
//+------------------------------------------------------------------+
| |||
void News_ThickLineAA(CCanvas &canvas, int x0, int y0, int x1, int y1, int thickness, uint argb)
| |||
{
| |||
//--- Clamp thickness to supported range
| |||
if(thickness < 1) thickness = 1;
| |||
if(thickness > 4) thickness = 4;
| |||
//--- Horizontal fast path
| |||
if(y0 == y1)
| |||
{
| |||
const int xL = MathMin(x0, x1);
| |||
const int xR = MathMax(x0, x1);
| |||
const int yTop = y0 - thickness / 2;
| |||
const int yBot = yTop + thickness - 1;
| |||
for(int yy = yTop; yy <= yBot; yy++)
| |||
for(int xx = xL; xx <= xR; xx++)
| |||
News_BlendPixel(canvas, xx, yy, argb);
| |||
return;
| |||
}
| |||
//--- Vertical fast path
| |||
if(x0 == x1)
| |||
{
| |||
const int yT = MathMin(y0, y1);
| |||
const int yB = MathMax(y0, y1);
| |||
const int xL = x0 - thickness / 2;
| |||
const int xR = xL + thickness - 1;
| |||
for(int xx = xL; xx <= xR; xx++)
| |||
for(int yy = yT; yy <= yB; yy++)
| |||
News_BlendPixel(canvas, xx, yy, argb);
| |||
return;
| |||
}
| |||
//--- Diagonal: supersampled coverage along the line normal
| |||
const double halfT = (double)thickness / 2.0;
| |||
const double ax = (double)x0 + 0.5, ay = (double)y0 + 0.5;
| |||
const double bx = (double)x1 + 0.5, by = (double)y1 + 0.5;
| |||
const double dx = bx - ax, dy = by - ay;
| |||
const double lenSq = dx * dx + dy * dy;
| |||
if(lenSq < 1e-9) return;
| |||
//--- Compute tight bounding box around line with padding
| |||
const double pad = halfT + 1.0;
| |||
const int bbL = (int)MathFloor(MathMin(ax, bx) - pad);
| |||
const int bbT = (int)MathFloor(MathMin(ay, by) - pad);
| |||
const int bbR = (int)MathCeil(MathMax(ax, bx) + pad);
| |||
const int bbB = (int)MathCeil(MathMax(ay, by) + pad);
| |||
const uchar bA = (uchar)((argb >> 24) & 0xFF);
| |||
const uint rgb = argb & 0x00FFFFFF;
| |||
const int sub = 4;
| |||
const double step = 1.0 / sub;
| |||
const int subSq = sub * sub;
| |||
//--- Sample each pixel in the bounding box
| |||
for(int py = bbT; py <= bbB; py++)
| |||
{
| |||
for(int px = bbL; px <= bbR; px++)
| |||
{
| |||
//--- Project pixel center onto line segment
| |||
const double pcx = (double)px + 0.5;
| |||
const double pcy = (double)py + 0.5;
| |||
double t = ((pcx - ax) * dx + (pcy - ay) * dy) / lenSq;
| |||
if(t < 0.0) t = 0.0;
| |||
if(t > 1.0) t = 1.0;
| |||
const double projX = ax + t * dx;
| |||
const double projY = ay + t * dy;
| |||
const double pdx = pcx - projX;
| |||
const double pdy = pcy - projY;
| |||
const double centerDist = MathSqrt(pdx * pdx + pdy * pdy);
| |||
if(centerDist > halfT + 1.0) continue;
| |||
//--- Fill fully-covered pixels directly
| |||
if(centerDist <= halfT - 1.0) { News_BlendPixel(canvas, px, py, argb); continue; }
| |||
//--- Supersample partial-coverage pixels
| |||
int inside = 0;
| |||
for(int sy = 0; sy < sub; sy++)
| |||
{
| |||
for(int sx = 0; sx < sub; sx++)
| |||
{
| |||
const double sx_ = (double)px + (sx + 0.5) * step;
| |||
const double sy_ = (double)py + (sy + 0.5) * step;
| |||
double st = ((sx_ - ax) * dx + (sy_ - ay) * dy) / lenSq;
| |||
if(st < 0.0) st = 0.0;
| |||
if(st > 1.0) st = 1.0;
| |||
const double spx = ax + st * dx;
| |||
const double spy = ay + st * dy;
| |||
const double sdx = sx_ - spx;
| |||
const double sdy = sy_ - spy;
| |||
if(sdx * sdx + sdy * sdy <= halfT * halfT) inside++;
| |||
}
| |||
}
| |||
if(inside == 0) continue;
| |||
//--- Blend coverage-weighted pixel
| |||
const uint covArgb = (((uint)(uchar)((int)bA * inside / subSq)) << 24) | rgb;
| |||
News_BlendPixel(canvas, px, py, covArgb);
| |||
}
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Chevron Direction Codes |
| |||
//+------------------------------------------------------------------+
| |||
#define NEWS_CHEVRON_UP 0 // Up chevron direction
| |||
#define NEWS_CHEVRON_DOWN 1 // Down chevron direction
| |||
#define NEWS_CHEVRON_LEFT 2 // Left chevron direction
| |||
#define NEWS_CHEVRON_RIGHT 3 // Right chevron direction
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Draw chevron centered at (cx, cy) pointing in given direction |
| |||
//+------------------------------------------------------------------+
| |||
void News_DrawChevron(CCanvas &canvas, int cx, int cy, int direction, uint argb)
| |||
{
| |||
//--- Draw two strokes forming a V-shape for the given direction
| |||
if(direction == NEWS_CHEVRON_UP)
| |||
{
| |||
News_ThickLineAA(canvas, cx - 4, cy + 2, cx, cy - 2, 2, argb);
| |||
News_ThickLineAA(canvas, cx, cy - 2, cx + 4, cy + 2, 2, argb);
| |||
}
| |||
else if(direction == NEWS_CHEVRON_DOWN)
| |||
{
| |||
News_ThickLineAA(canvas, cx - 4, cy - 2, cx, cy + 2, 2, argb);
| |||
News_ThickLineAA(canvas, cx, cy + 2, cx + 4, cy - 2, 2, argb);
| |||
}
| |||
else if(direction == NEWS_CHEVRON_LEFT)
| |||
{
| |||
News_ThickLineAA(canvas, cx + 2, cy - 4, cx - 2, cy, 2, argb);
| |||
News_ThickLineAA(canvas, cx - 2, cy, cx + 2, cy + 4, 2, argb);
| |||
}
| |||
else
| |||
{
| |||
//--- Draw right-pointing chevron strokes
| |||
News_ThickLineAA(canvas, cx - 2, cy - 4, cx + 2, cy, 2, argb);
| |||
News_ThickLineAA(canvas, cx + 2, cy, cx - 2, cy + 4, 2, argb);
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Draw equilateral triangle centered at (cx, cy) |
| |||
//+------------------------------------------------------------------+
| |||
void News_DrawTriangle(CCanvas &canvas, int cx, int cy, int direction, uint argb)
| |||
{
| |||
//--- Define equilateral triangle geometry; side=7, height=side*sqrt(3)/2
| |||
const double side = 7.0;
| |||
const double height = side * 0.86602540378;
| |||
const double halfBase = side / 2.0;
| |||
const uchar bA = (uchar)((argb >> 24) & 0xFF);
| |||
const uint rgb = argb & 0x00FFFFFF;
| |||
const int sub = 8;
| |||
const double step = 1.0 / sub;
| |||
const int subSq = sub * sub;
| |||
//--- Set bounding box with padding for AA edge coverage
| |||
const int rad = 5;
| |||
const int bbL = cx - rad;
| |||
const int bbR = cx + rad;
| |||
const int bbT = cy - rad;
| |||
const int bbB = cy + rad;
| |||
//--- Compute centroid offset and apex/axis vectors for this direction
| |||
const double cz = height / 3.0;
| |||
double apexX, apexY, axisX, axisY, perpX, perpY;
| |||
if(direction == NEWS_CHEVRON_UP)
| |||
{
| |||
apexX = (double)cx; apexY = (double)cy - (height - cz);
| |||
axisX = 0.0; axisY = 1.0;
| |||
perpX = 1.0; perpY = 0.0;
| |||
}
| |||
else if(direction == NEWS_CHEVRON_DOWN)
| |||
{
| |||
apexX = (double)cx; apexY = (double)cy + (height - cz);
| |||
axisX = 0.0; axisY = -1.0;
| |||
perpX = 1.0; perpY = 0.0;
| |||
}
| |||
else if(direction == NEWS_CHEVRON_LEFT)
| |||
{
| |||
apexX = (double)cx - (height - cz); apexY = (double)cy;
| |||
axisX = 1.0; axisY = 0.0;
| |||
perpX = 0.0; perpY = 1.0;
| |||
}
| |||
else
| |||
{
| |||
//--- Right-facing apex
| |||
apexX = (double)cx + (height - cz); apexY = (double)cy;
| |||
axisX = -1.0; axisY = 0.0;
| |||
perpX = 0.0; perpY = 1.0;
| |||
}
| |||
//--- Supersample each pixel in the bounding box
| |||
for(int py = bbT; py <= bbB; py++)
| |||
{
| |||
for(int px = bbL; px <= bbR; px++)
| |||
{
| |||
int inside = 0;
| |||
for(int sy = 0; sy < sub; sy++)
| |||
{
| |||
for(int sx = 0; sx < sub; sx++)
| |||
{
| |||
const double sxp = (double)px + (sx + 0.5) * step;
| |||
const double syp = (double)py + (sy + 0.5) * step;
| |||
const double ddx = sxp - apexX;
| |||
const double ddy = syp - apexY;
| |||
const double along = ddx * axisX + ddy * axisY;
| |||
if(along < 0.0 || along > height) continue;
| |||
const double perp = MathAbs(ddx * perpX + ddy * perpY);
| |||
const double maxPerp = halfBase * (along / height);
| |||
if(perp <= maxPerp) inside++;
| |||
}
| |||
}
| |||
if(inside == 0) continue;
| |||
//--- Blend fully-covered or partial-coverage pixel
| |||
if(inside == subSq)
| |||
News_BlendPixel(canvas, px, py, argb);
| |||
else
| |||
{
| |||
const uint covArgb = (((uint)(uchar)((int)bA * inside / subSq)) << 24) | rgb;
| |||
News_BlendPixel(canvas, px, py, covArgb);
| |||
}
| |||
}
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Draw filled anti-aliased circle for impact dot |
| |||
//+------------------------------------------------------------------+
| |||
void News_DrawFilledCircle(CCanvas &canv, int cx, int cy, int radius, uint argb)
| |||
{
| |||
const double rd = (double)radius;
| |||
const double rd2 = rd * rd;
| |||
//--- Iterate over the bounding square
| |||
for(int dy = -radius; dy <= radius; dy++)
| |||
{
| |||
for(int dx = -radius; dx <= radius; dx++)
| |||
{
| |||
const double dist2 = (double)(dx * dx + dy * dy);
| |||
//--- Fill pixels fully inside the circle
| |||
if(dist2 <= rd2 - 2.0 * rd + 1.0)
| |||
{
| |||
canv.PixelSet(cx + dx, cy + dy, argb);
| |||
}
| |||
//--- Anti-alias pixels on the circle edge
| |||
else if(dist2 <= rd2 + 2.0 * rd + 1.0)
| |||
{
| |||
const double dist = MathSqrt(dist2);
| |||
const double cover = MathMax(0.0, MathMin(1.0, rd - dist + 0.5));
| |||
if(cover > 0.0)
| |||
{
| |||
const uchar baseA = (uchar)((argb >> 24) & 0xFF);
| |||
const uchar a = (uchar)(baseA * cover);
| |||
const uint blended = (a << 24) | (argb & 0x00FFFFFF);
| |||
News_BlendPixel(canv, cx + dx, cy + dy, blended);
| |||
}
| |||
}
| |||
}
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Draw small two-stroke checkmark icon |
| |||
//+------------------------------------------------------------------+
| |||
void News_DrawCheckIcon(CCanvas &canv, int x, int y, int size, uint argb)
| |||
{
| |||
//--- Compute stroke thickness and checkmark vertex positions
| |||
const int t = MathMax(1, size / 7);
| |||
const int p1x = x + size / 5;
| |||
const int p1y = y + size / 2;
| |||
const int p2x = x + size * 2 / 5;
| |||
const int p2y = y + size * 3 / 4;
| |||
const int p3x = x + size * 4 / 5;
| |||
const int p3y = y + size / 4;
| |||
//--- Draw two anti-aliased strokes with thickness offsets
| |||
for(int dx = -t / 2; dx <= t / 2; dx++)
| |||
{
| |||
for(int dy = -t / 2; dy <= t / 2; dy++)
| |||
{
| |||
canv.LineAA(p1x + dx, p1y + dy, p2x + dx, p2y + dy, argb);
| |||
canv.LineAA(p2x + dx, p2y + dy, p3x + dx, p3y + dy, argb);
| |||
}
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Draw small X-shaped cross icon |
| |||
//+------------------------------------------------------------------+
| |||
void News_DrawCrossIcon(CCanvas &canv, int x, int y, int size, uint argb)
| |||
{
| |||
//--- Compute stroke thickness and padded corner positions
| |||
const int t = MathMax(1, size / 7);
| |||
const int pad = size / 5;
| |||
//--- Draw two diagonal strokes with thickness offsets
| |||
for(int dx = -t / 2; dx <= t / 2; dx++)
| |||
{
| |||
for(int dy = -t / 2; dy <= t / 2; dy++)
| |||
{
| |||
canv.LineAA(x + pad + dx, y + pad + dy,
| |||
x + size - pad + dx, y + size - pad + dy, argb);
| |||
canv.LineAA(x + size - pad + dx, y + pad + dy,
| |||
x + pad + dx, y + size - pad + dy, argb);
| |||
}
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Currency Filter Constants |
| |||
//+------------------------------------------------------------------+
| |||
const string NEWS_CURRENCIES[] = {"AUD", "CAD", "CHF", "EUR", "GBP", "JPY", "NZD", "USD"};
| |||
#define NEWS_CURR_COUNT 8
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Impact Level Constants |
| |||
//+------------------------------------------------------------------+
| |||
const string NEWS_IMPACT_LABELS[] = {"None", "Low", "Medium", "High"};
| |||
const ENUM_CALENDAR_EVENT_IMPORTANCE NEWS_IMPACT_LEVELS[] =
| |||
{
| |||
CALENDAR_IMPORTANCE_NONE, CALENDAR_IMPORTANCE_LOW,
| |||
CALENDAR_IMPORTANCE_MODERATE, CALENDAR_IMPORTANCE_HIGH
| |||
};
| |||
#define NEWS_IMPACT_COUNT 4
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Economic event record |
| |||
//+------------------------------------------------------------------+
| |||
struct NewsEvent
| |||
{
| |||
string eventDate; // Date string YYYY.MM.DD
| |||
string eventTime; // Time string HH:MM
| |||
string currency; // Currency code
| |||
string event; // Event name
| |||
string importance; // None / Low / Medium / High
| |||
double actual; // Actual released value
| |||
double forecast; // Forecast value
| |||
double previous; // Previous value
| |||
double revisedPrevious; // Revised previous value
| |||
bool hasActual; // True when actual value is published
| |||
bool hasForecast; // True when forecast value is published
| |||
bool hasPrevious; // True when previous value is published
| |||
bool hasRevised; // True when revised previous is published
| |||
int unit; // ENUM_CALENDAR_EVENT_UNIT
| |||
int multiplier; // ENUM_CALENDAR_EVENT_MULTIPLIER
| |||
int digits; // Decimal places for display
| |||
datetime eventDateTime; // Combined event timestamp
| |||
long eventId; // Unique event ID for trade tracking
| |||
};
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Event Arrays |
| |||
//+------------------------------------------------------------------+
| |||
NewsEvent g_news_allEvents[]; // All loaded events from CSV or live feed
| |||
NewsEvent g_news_filteredEvents[]; // Events filtered by date range
| |||
NewsEvent g_news_displayableEvents[]; // Events after all active filters applied
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Row Plan Constants |
| |||
//+------------------------------------------------------------------+
| |||
#define NEWS_ROW_KIND_DAY 0 // Row represents a day-separator header
| |||
#define NEWS_ROW_KIND_EVENT 1 // Row represents an individual event entry
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Row plan entry structure |
| |||
//+------------------------------------------------------------------+
| |||
struct NewsRowEntry
| |||
{
| |||
int kind; // NEWS_ROW_KIND_DAY or NEWS_ROW_KIND_EVENT
| |||
int eventIdx; // Index into g_news_displayableEvents when kind==EVENT
| |||
string label; // Day label string when kind==DAY
| |||
string dateKey; // Date string used for collapse toggle
| |||
};
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Row Plan and Collapse State |
| |||
//+------------------------------------------------------------------+
| |||
NewsRowEntry g_news_rowPlan[]; // Render plan built from displayable events
| |||
string g_news_collapsedDays[]; // Date strings of currently collapsed day groups
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Filter Selection State |
| |||
//+------------------------------------------------------------------+
| |||
bool g_news_currSelected[NEWS_CURR_COUNT]; // Per-currency selection flag
| |||
bool g_news_impactSelected[NEWS_IMPACT_COUNT]; // Per-impact selection flag
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Filter Master Toggles |
| |||
//+------------------------------------------------------------------+
| |||
bool g_news_filterCurrencyOn = true; // Currency filter enabled flag
| |||
bool g_news_filterImpactOn = true; // Impact filter enabled flag
| |||
bool g_news_filterTimeOn = true; // Time range filter enabled flag
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Event Counters |
| |||
//+------------------------------------------------------------------+
| |||
int g_news_totalConsidered = 0; // Total events considered before filtering
| |||
int g_news_totalFiltered = 0; // Total events after range filter
| |||
int g_news_totalDisplayable = 0; // Total events after all filters
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Dashboard Position and Visibility State |
| |||
//+------------------------------------------------------------------+
| |||
int g_news_dashboardX = 50; // Dashboard X position on chart
| |||
int g_news_dashboardY = 50; // Dashboard Y position on chart
| |||
bool g_news_dashboardVisible = false; // Dashboard visibility flag
| |||
bool g_news_canvasExists = false; // Canvas created flag
| |||
bool g_news_dragging = false; // Drag-move in progress flag
| |||
int g_news_dragOffsetX = 0; // X offset from dashboard origin at drag start
| |||
int g_news_dragOffsetY = 0; // Y offset from dashboard origin at drag start
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Table Scratch Canvas |
| |||
//+------------------------------------------------------------------+
| |||
CNewsCanvasFast g_news_tableTmp; // Offscreen scratch canvas for row clipping
| |||
bool g_news_tableTmpReady = false; // Scratch canvas ready flag
| |||
int g_news_tableTmpW = 0; // Scratch canvas width
| |||
int g_news_tableTmpH = 0; // Scratch canvas height
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Mouse State |
| |||
//+------------------------------------------------------------------+
| |||
int g_news_mouseLx = -1; // Cached canvas-local mouse X
| |||
int g_news_mouseLy = -1; // Cached canvas-local mouse Y
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Hover Codes |
| |||
//+------------------------------------------------------------------+
| |||
#define NEWS_HOV_NONE 0 // No element hovered
| |||
#define NEWS_HOV_CLOSE 1 // Close button hovered
| |||
#define NEWS_HOV_THEME 2 // Theme toggle hovered
| |||
#define NEWS_HOV_DRAG 3 // Drag handle hovered
| |||
#define NEWS_HOV_RESIZE_R 4 // Right-edge resize zone hovered
| |||
#define NEWS_HOV_RESIZE_B 5 // Bottom-edge resize zone hovered
| |||
#define NEWS_HOV_FILTER_CURR 10 // Currency filter toggle hovered
| |||
#define NEWS_HOV_FILTER_IMP 11 // Impact filter toggle hovered
| |||
#define NEWS_HOV_FILTER_TIME 12 // Time filter toggle hovered
| |||
#define NEWS_HOV_CURR_BASE 100 // Currency chip base offset (+0..7)
| |||
#define NEWS_HOV_IMP_BASE 200 // Impact pill base offset (+0..3)
| |||
#define NEWS_HOV_ROW_BASE 300 // Event row base offset (+0..N-1)
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Interaction and Change Tracking State |
| |||
//+------------------------------------------------------------------+
| |||
int g_news_hover = NEWS_HOV_NONE; // Current hover code
| |||
int g_news_lastEventCount = -1; // Event count at last redraw
| |||
datetime g_news_lastEventStamp = 0; // Timestamp at last event load
| |||
bool g_news_filtersChanged = true; // Filters dirty flag
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Trade State |
| |||
//+------------------------------------------------------------------+
| |||
bool g_news_tradeExecuted = false; // Trade already fired flag
| |||
datetime g_news_tradedNewsTime = 0; // Timestamp of the traded news event
| |||
long g_news_triggeredIds[]; // IDs of events that already fired trades
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Toast State |
| |||
//+------------------------------------------------------------------+
| |||
string g_news_toastText = ""; // Current toast message text
| |||
bool g_news_toastIsError = false; // True when toast represents an error
| |||
ulong g_news_toastExpiryMs = 0; // Toast expiry tick count in milliseconds
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Timer State |
| |||
//+------------------------------------------------------------------+
| |||
ulong g_news_lastBlinkMs = 0; // Tick count of last blink cycle
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Set toast notification and start 5-second expiry |
| |||
//+------------------------------------------------------------------+
| |||
void News_ShowToast(string text, bool isError)
| |||
{
| |||
//--- Store message and error flag
| |||
g_news_toastText = text;
| |||
g_news_toastIsError = isError;
| |||
//--- Schedule expiry 5 seconds from now
| |||
g_news_toastExpiryMs = GetTickCount64() + 5000;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Initialize default filter selections - all on |
| |||
//+------------------------------------------------------------------+
| |||
void News_InitDefaultFilters()
| |||
{
| |||
//--- Enable all currency filters
| |||
for(int i = 0; i < NEWS_CURR_COUNT; i++) g_news_currSelected[i] = true;
| |||
//--- Enable all impact filters
| |||
for(int i = 0; i < NEWS_IMPACT_COUNT; i++) g_news_impactSelected[i] = true;
| |||
//--- Enable all master filter toggles
| |||
g_news_filterCurrencyOn = true;
| |||
g_news_filterImpactOn = true;
| |||
g_news_filterTimeOn = true;
| |||
//--- Mark filters as dirty to trigger rebuild
| |||
g_news_filtersChanged = true;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Scroll state structure |
| |||
//+------------------------------------------------------------------+
| |||
struct NewsScrollState
| |||
{
| |||
int scrollPx; // Current scroll offset in pixels
| |||
int totalH; // Total content height in pixels
| |||
int viewportH; // Visible viewport height
| |||
int trackL; // Track left X
| |||
int trackT; // Track top Y
| |||
int trackR; // Track right X
| |||
int trackB; // Track bottom Y
| |||
bool dragging; // Drag in progress flag
| |||
int dragStartY; // Mouse Y at drag start
| |||
int dragStartScroll; // scrollPx value at drag start
| |||
bool hover; // Cursor over track or thumb
| |||
bool hoveredArea; // Cursor over track area
| |||
bool hoveredThumb; // Cursor over thumb specifically
| |||
};
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Initialize scroll state to defaults |
| |||
//+------------------------------------------------------------------+
| |||
void News_ScrollInit(NewsScrollState &s)
| |||
{
| |||
//--- Zero all scroll fields
| |||
s.scrollPx = 0;
| |||
s.totalH = 0;
| |||
s.viewportH = 0;
| |||
s.trackL = 0;
| |||
s.trackT = 0;
| |||
s.trackR = 0;
| |||
s.trackB = 0;
| |||
s.dragging = false;
| |||
s.dragStartY = 0;
| |||
s.dragStartScroll = 0;
| |||
s.hover = false;
| |||
s.hoveredArea = false;
| |||
s.hoveredThumb = false;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Compute maximum scroll position |
| |||
//+------------------------------------------------------------------+
| |||
int News_ScrollMax(NewsScrollState &s)
| |||
{
| |||
//--- Return positive overflow or zero
| |||
const int m = s.totalH - s.viewportH;
| |||
return (m > 0) ? m : 0;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Clamp current scroll position to valid range |
| |||
//+------------------------------------------------------------------+
| |||
void News_ScrollClamp(NewsScrollState &s)
| |||
{
| |||
//--- Enforce lower and upper scroll bounds
| |||
const int m = News_ScrollMax(s);
| |||
if(s.scrollPx < 0) s.scrollPx = 0;
| |||
if(s.scrollPx > m) s.scrollPx = m;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Test if scrollbar should be visible |
| |||
//+------------------------------------------------------------------+
| |||
bool News_ScrollVisible(NewsScrollState &s)
| |||
{
| |||
//--- Scrollbar appears only when content exceeds viewport
| |||
return s.totalH > s.viewportH;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Compute thumb rectangle within track |
| |||
//+------------------------------------------------------------------+
| |||
void News_ScrollThumbRect(NewsScrollState &s, int &outT, int &outB)
| |||
{
| |||
//--- Handle degenerate track
| |||
const int trackH = s.trackB - s.trackT;
| |||
if(trackH <= 0 || s.totalH <= 0)
| |||
{
| |||
outT = s.trackT; outB = s.trackB;
| |||
return;
| |||
}
| |||
//--- Compute thumb height proportional to viewport/total ratio
| |||
const double ratio = (double)s.viewportH / (double)s.totalH;
| |||
int thumbH = (int)(trackH * ratio);
| |||
if(thumbH < 20) thumbH = 20;
| |||
if(thumbH > trackH) thumbH = trackH;
| |||
//--- Position thumb proportional to current scroll offset
| |||
const int m = News_ScrollMax(s);
| |||
const double scrollRatio = (m > 0) ? ((double)s.scrollPx / (double)m) : 0.0;
| |||
const int avail = trackH - thumbH;
| |||
outT = s.trackT + (int)(avail * scrollRatio);
| |||
outB = outT + thumbH;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Hit-test scrollbar thumb |
| |||
//+------------------------------------------------------------------+
| |||
bool News_ScrollHitThumb(NewsScrollState &s, int x, int y)
| |||
{
| |||
//--- Reject points outside the track lane
| |||
if(x < s.trackL - 2 || x > s.trackR + 2) return false;
| |||
//--- Test Y against current thumb bounds
| |||
int tT, tB;
| |||
News_ScrollThumbRect(s, tT, tB);
| |||
return (y >= tT && y < tB);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Begin scroll drag from mouse y position |
| |||
//+------------------------------------------------------------------+
| |||
void News_ScrollBeginDrag(NewsScrollState &s, int y)
| |||
{
| |||
//--- Record drag start state
| |||
s.dragging = true;
| |||
s.dragStartY = y;
| |||
s.dragStartScroll = s.scrollPx;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Update drag with current mouse y |
| |||
//+------------------------------------------------------------------+
| |||
void News_ScrollUpdateDrag(NewsScrollState &s, int y)
| |||
{
| |||
//--- Abort if not in drag state
| |||
if(!s.dragging) return;
| |||
const int trackH = s.trackB - s.trackT;
| |||
if(trackH <= 0) return;
| |||
//--- Convert pixel delta to scroll units and apply
| |||
const int dy = y - s.dragStartY;
| |||
const double scale = (double)s.totalH / (double)trackH;
| |||
const int newScroll = s.dragStartScroll + (int)(dy * scale);
| |||
s.scrollPx = newScroll;
| |||
News_ScrollClamp(s);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| End scroll drag |
| |||
//+------------------------------------------------------------------+
| |||
void News_ScrollEndDrag(NewsScrollState &s)
| |||
{
| |||
//--- Clear drag flag
| |||
s.dragging = false;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Apply mouse wheel delta to scroll position |
| |||
//+------------------------------------------------------------------+
| |||
void News_ScrollByWheel(NewsScrollState &s, int delta, int step)
| |||
{
| |||
//--- Scroll up when delta is positive, down when negative
| |||
if(delta > 0) s.scrollPx -= step;
| |||
else s.scrollPx += step;
| |||
News_ScrollClamp(s);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Draw scrollbar with fully-rounded thumb onto canvas |
| |||
//+------------------------------------------------------------------+
| |||
void News_ScrollDraw(CCanvas &canv, NewsScrollState &s)
| |||
{
| |||
//--- Skip when scrollbar is not needed
| |||
if(!News_ScrollVisible(s)) return;
| |||
const int trackW = s.trackR - s.trackL;
| |||
if(trackW <= 0) return;
| |||
//--- Compute thumb position
| |||
int tT, tB;
| |||
News_ScrollThumbRect(s, tT, tB);
| |||
const int thumbH = tB - tT;
| |||
if(thumbH <= 0) return;
| |||
//--- Pick thumb color based on current interaction state
| |||
color thumbColor = g_news_scrollSlider;
| |||
if(s.dragging) thumbColor = g_news_scrollSliderDrag;
| |||
else if(s.hoveredThumb || s.hover) thumbColor = g_news_scrollSliderHover;
| |||
//--- Draw fully-rounded thumb with radius equal to half track width
| |||
const int radius = trackW / 2;
| |||
News_FillRoundRect(canv, s.trackL, tT, trackW, thumbH, radius, ColorToARGB(thumbColor, 255));
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Global Scroll State |
| |||
//+------------------------------------------------------------------+
| |||
NewsScrollState g_news_tableScroll; // Scroll state for the event table
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Format datetime as "HH:MM" string |
| |||
//+------------------------------------------------------------------+
| |||
string News_FormatTimeShort(datetime t)
| |||
{
| |||
//--- Delegate to built-in time formatter with minutes precision
| |||
return TimeToString(t, TIME_MINUTES);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get impact color for a label string |
| |||
//+------------------------------------------------------------------+
| |||
color News_GetImpactColor(string label)
| |||
{
| |||
//--- Map label to corresponding theme color
| |||
if(label == "High") return g_news_impHigh;
| |||
if(label == "Medium") return g_news_impMed;
| |||
if(label == "Low") return g_news_impLow;
| |||
return g_news_impNone;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get importance enum from label string |
| |||
//+------------------------------------------------------------------+
| |||
ENUM_CALENDAR_EVENT_IMPORTANCE News_GetImpactEnum(string label)
| |||
{
| |||
//--- Map label to corresponding enum value
| |||
if(label == "High") return CALENDAR_IMPORTANCE_HIGH;
| |||
if(label == "Medium") return CALENDAR_IMPORTANCE_MODERATE;
| |||
if(label == "Low") return CALENDAR_IMPORTANCE_LOW;
| |||
return CALENDAR_IMPORTANCE_NONE;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get importance label from enum value |
| |||
//+------------------------------------------------------------------+
| |||
string News_GetImpactLabel(ENUM_CALENDAR_EVENT_IMPORTANCE imp)
| |||
{
| |||
//--- Map enum to display label string
| |||
if(imp == CALENDAR_IMPORTANCE_HIGH) return "High";
| |||
if(imp == CALENDAR_IMPORTANCE_MODERATE) return "Medium";
| |||
if(imp == CALENDAR_IMPORTANCE_LOW) return "Low";
| |||
return "None";
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Format calendar value with unit, multiplier, and digits metadata |
| |||
//+------------------------------------------------------------------+
| |||
string News_FormatValue(bool hasValue, double v, int unit, int multiplier, int digits)
| |||
{
| |||
//--- Return dash when value is not published
| |||
if(!hasValue) return "-";
| |||
//--- Determine multiplier suffix
| |||
string suffix = "";
| |||
if(multiplier == CALENDAR_MULTIPLIER_THOUSANDS) suffix = "K";
| |||
else if(multiplier == CALENDAR_MULTIPLIER_MILLIONS) suffix = "M";
| |||
else if(multiplier == CALENDAR_MULTIPLIER_BILLIONS) suffix = "B";
| |||
else if(multiplier == CALENDAR_MULTIPLIER_TRILLIONS) suffix = "T";
| |||
//--- Clamp decimal places to valid range
| |||
int d = digits;
| |||
if(d < 0) d = 0;
| |||
if(d > 6) d = 6;
| |||
string num = DoubleToString(v, d);
| |||
//--- Apply currency prefix for USD unit
| |||
if(unit == CALENDAR_UNIT_USD) return "$" + num + suffix;
| |||
//--- Apply percent suffix for percent unit
| |||
if(unit == CALENDAR_UNIT_PERCENT) return num + "%" + suffix;
| |||
//--- Map remaining units to their textual suffix
| |||
string post = "";
| |||
if(unit == CALENDAR_UNIT_HOUR) post = " h";
| |||
else if(unit == CALENDAR_UNIT_JOB) post = " jobs";
| |||
else if(unit == CALENDAR_UNIT_RIG) post = " rigs";
| |||
else if(unit == CALENDAR_UNIT_PEOPLE) post = " ppl";
| |||
else if(unit == CALENDAR_UNIT_MORTGAGE) post = " mtg";
| |||
else if(unit == CALENDAR_UNIT_VOTE) post = " votes";
| |||
else if(unit == CALENDAR_UNIT_BARREL) post = " bbl";
| |||
else if(unit == CALENDAR_UNIT_CUBICFEET) post = " cf";
| |||
else if(unit == CALENDAR_UNIT_POSITION) post = " pos";
| |||
else if(unit == CALENDAR_UNIT_BUILDING) post = " bldg";
| |||
else if(unit == CALENDAR_UNIT_CURRENCY) post = "";
| |||
//--- Return number with suffix and unit postfix
| |||
return num + suffix + post;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Format time-remaining string for an event row |
| |||
//+------------------------------------------------------------------+
| |||
string News_FormatRemain(datetime evTime, datetime now)
| |||
{
| |||
//--- Compute signed difference in seconds
| |||
const long diffSec = (long)evTime - (long)now;
| |||
//--- Show static label for past events
| |||
if(diffSec < -60) return "Released";
| |||
//--- Show "now" within 60 seconds of release
| |||
const long absSec = (diffSec < 0) ? -diffSec : diffSec;
| |||
if(absSec <= 60) return "now";
| |||
//--- Decompose absolute seconds into days, hours, minutes, seconds
| |||
const long mins = absSec / 60;
| |||
const long hours = mins / 60;
| |||
const long days = hours / 24;
| |||
string body;
| |||
if(days > 0)
| |||
{
| |||
const long remH = hours - days * 24;
| |||
body = IntegerToString(days) + "d " + IntegerToString(remH) + "h";
| |||
}
| |||
else if(hours > 0)
| |||
{
| |||
const long remM = mins - hours * 60;
| |||
body = IntegerToString(hours) + "h " + IntegerToString(remM) + "m";
| |||
}
| |||
else if(mins > 0)
| |||
{
| |||
const long remS = absSec - mins * 60;
| |||
body = IntegerToString(mins) + "m " + IntegerToString(remS) + "s";
| |||
}
| |||
else
| |||
{
| |||
body = IntegerToString(absSec) + "s";
| |||
}
| |||
//--- Prefix with "in " for future events
| |||
return "in " + body;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Test if event is upcoming and within 30 minutes |
| |||
//+------------------------------------------------------------------+
| |||
bool News_RemainIsImminent(datetime evTime, datetime now)
| |||
{
| |||
//--- Return true only when event is in the future and within 30 min
| |||
const long diffSec = (long)evTime - (long)now;
| |||
return (diffSec > 0 && diffSec <= 30 * 60);
| |||
}
| |||
| |||
#endif // NEWS_CORE_MQH
|