1683 lines
No EOL
77 KiB
MQL5
1683 lines
No EOL
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 |