// *****************************************************************************
// * This file is part of the FreeFileSync project. It is distributed under    *
// * GNU General Public License: http://www.gnu.org/licenses/gpl-3.0           *
// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
// *****************************************************************************

#include "grid.h"
#include <cassert>
#include <set>
#include <chrono>
#include <wx/settings.h>
#include <wx/listbox.h>
#include <wx/tooltip.h>
#include <wx/timer.h>
#include <wx/utils.h>
//#include <zen/tick_count.h>
#include <zen/string_tools.h>
#include <zen/scope_guard.h>
#include <zen/utf.h>
#include <zen/format_unit.h>
#include "dc.h"

    #include <gtk/gtk.h>

using namespace zen;


wxColor Grid::getColorSelectionGradientFrom() { return { 137, 172, 255 }; } //blue: HSL: 158, 255, 196   HSV: 222, 0.46, 1
wxColor Grid::getColorSelectionGradientTo  () { return { 225, 234, 255 }; } //      HSL: 158, 255, 240   HSV: 222, 0.12, 1

const int GridData::COLUMN_GAP_LEFT = 4;


void zen::clearArea(wxDC& dc, const wxRect& rect, const wxColor& col)
{
    wxDCPenChanger   dummy (dc, col);
    wxDCBrushChanger dummy2(dc, col);
    dc.DrawRectangle(rect);
}


namespace
{
//let's NOT create wxWidgets objects statically:
//------------------------------ Grid Parameters --------------------------------
inline wxColor getColorLabelText() { return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); }
inline wxColor getColorGridLine() { return { 192, 192, 192 }; } //light grey

inline wxColor getColorLabelGradientFrom() { return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); }
inline wxColor getColorLabelGradientTo  () { return { 200, 200, 200 }; } //light grey

inline wxColor getColorLabelGradientFocusFrom() { return getColorLabelGradientFrom(); }
inline wxColor getColorLabelGradientFocusTo  () { return Grid::getColorSelectionGradientFrom(); }

const double MOUSE_DRAG_ACCELERATION = 1.5; //unit: [rows / (pixel * sec)] -> same value like Explorer!
const int DEFAULT_COL_LABEL_BORDER = 6; //top + bottom border in addition to label height
const int COLUMN_MOVE_DELAY        = 5;  //unit: [pixel] (from Explorer)
const int COLUMN_MIN_WIDTH         = 40; //only honored when resizing manually!
const int ROW_LABEL_BORDER         = 3;
const int COLUMN_RESIZE_TOLERANCE  = 6; //unit [pixel]
const int COLUMN_FILL_GAP_TOLERANCE = 10; //enlarge column to fill full width when resizing

const bool fillGapAfterColumns = true; //draw rows/column label to fill full window width; may become an instance variable some time?
}

//----------------------------------------------------------------------------------------------------------------
const wxEventType zen::EVENT_GRID_MOUSE_LEFT_DOUBLE     = wxNewEventType();
const wxEventType zen::EVENT_GRID_MOUSE_LEFT_DOWN       = wxNewEventType();
const wxEventType zen::EVENT_GRID_MOUSE_LEFT_UP         = wxNewEventType();
const wxEventType zen::EVENT_GRID_MOUSE_RIGHT_DOWN      = wxNewEventType();
const wxEventType zen::EVENT_GRID_MOUSE_RIGHT_UP        = wxNewEventType();
const wxEventType zen::EVENT_GRID_SELECT_RANGE          = wxNewEventType();
const wxEventType zen::EVENT_GRID_COL_LABEL_MOUSE_LEFT  = wxNewEventType();
const wxEventType zen::EVENT_GRID_COL_LABEL_MOUSE_RIGHT = wxNewEventType();
const wxEventType zen::EVENT_GRID_COL_RESIZE            = wxNewEventType();
//----------------------------------------------------------------------------------------------------------------

void GridData::renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected)
{
    drawCellBackground(dc, rect, enabled, selected, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
}


void GridData::renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover)
{
    wxRect rectTmp = drawCellBorder(dc, rect);

    rectTmp.x     += COLUMN_GAP_LEFT;
    rectTmp.width -= COLUMN_GAP_LEFT;
    drawCellText(dc, rectTmp, getValue(row, colType));
}


int GridData::getBestSize(wxDC& dc, size_t row, ColumnType colType)
{
    return dc.GetTextExtent(getValue(row, colType)).GetWidth() + 2 * COLUMN_GAP_LEFT + 1; //gap on left and right side + border
}


wxRect GridData::drawCellBorder(wxDC& dc, const wxRect& rect) //returns remaining rectangle
{
    wxDCPenChanger dummy2(dc, getColorGridLine());
    dc.DrawLine(rect.GetBottomLeft(),  rect.GetBottomRight());
    dc.DrawLine(rect.GetBottomRight(), rect.GetTopRight() + wxPoint(0, -1));

    return wxRect(rect.GetTopLeft(), wxSize(rect.width - 1, rect.height - 1));
}


void GridData::drawCellBackground(wxDC& dc, const wxRect& rect, bool enabled, bool selected, const wxColor& backgroundColor)
{
    if (enabled)
    {
        if (selected)
            dc.GradientFillLinear(rect, Grid::getColorSelectionGradientFrom(), Grid::getColorSelectionGradientTo(), wxEAST);
        else
            clearArea(dc, rect, backgroundColor);
    }
    else
        clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE));
}


wxSize GridData::drawCellText(wxDC& dc, const wxRect& rect, const std::wstring& text, int alignment)
{
    /*
    performance notes (Windows):
    - wxDC::GetTextExtent() is by far the most expensive call (20x more expensive than wxDC::DrawText())
    - wxDC::DrawLabel() is inefficiently implemented; internally calls: wxDC::GetMultiLineTextExtent(), wxDC::GetTextExtent(), wxDC::DrawText()
    - wxDC::GetMultiLineTextExtent() calls wxDC::GetTextExtent()
    - wxDC::DrawText also calls wxDC::GetTextExtent()!!
    => wxDC::DrawLabel() boils down to 3(!) calls to wxDC::GetTextExtent()!!!
    - wxDC::DrawLabel results in GetTextExtent() call even for empty strings!!!
    => skip the wxDC::DrawLabel() cruft and directly call wxDC::DrawText!
    */

    //truncate large texts and add ellipsis
    assert(!contains(text, L"\n"));
    const wchar_t ELLIPSIS = L'\u2026'; //"..."

    std::wstring textTrunc = text;
    wxSize extentTrunc = dc.GetTextExtent(textTrunc);
    if (extentTrunc.GetWidth() > rect.width)
    {
        //unlike Windows 7 Explorer, we truncate UTF-16 correctly: e.g. CJK-Ideogramm encodes to TWO wchar_t: utfTo<std::wstring>("\xf0\xa4\xbd\x9c");
        size_t low  = 0;                   //number of unicode chars!
        size_t high = unicodeLength(text); //
        if (high > 1)
            for (;;)
            {
                const size_t middle = (low + high) / 2; //=> never 0 when "high - low > 1"
                if (high - low <= 1)
                {
                    if (low == 0)
                    {
                        textTrunc   = ELLIPSIS;
                        extentTrunc = dc.GetTextExtent(ELLIPSIS);
                    }
                    break;
                }

                const std::wstring& candidate = getUnicodeSubstring(text, 0, middle) + ELLIPSIS;
                const wxSize extentCand = dc.GetTextExtent(candidate); //perf: most expensive call of this routine!

                if (extentCand.GetWidth() <= rect.width)
                {
                    low = middle;
                    textTrunc   = candidate;
                    extentTrunc = extentCand;
                }
                else
                    high = middle;
            }
    }

    wxPoint pt = rect.GetTopLeft();
    if (alignment & wxALIGN_RIGHT) //note: wxALIGN_LEFT == 0!
        pt.x += rect.width - extentTrunc.GetWidth();
    else if (alignment & wxALIGN_CENTER_HORIZONTAL)
        pt.x += (rect.width - extentTrunc.GetWidth()) / 2;

    if (alignment & wxALIGN_BOTTOM) //note: wxALIGN_TOP == 0!
        pt.y += rect.height - extentTrunc.GetHeight();
    else if (alignment & wxALIGN_CENTER_VERTICAL)
        pt.y += (rect.height - extentTrunc.GetHeight()) / 2;

    RecursiveDcClipper clip(dc, rect);
    dc.DrawText(textTrunc, pt);
    return extentTrunc;
}


void GridData::renderColumnLabel(Grid& grid, wxDC& dc, const wxRect& rect, ColumnType colType, bool highlighted)
{
    wxRect rectTmp = drawColumnLabelBorder(dc, rect);
    drawColumnLabelBackground(dc, rectTmp, highlighted);

    rectTmp.x     += COLUMN_GAP_LEFT;
    rectTmp.width -= COLUMN_GAP_LEFT;
    drawColumnLabelText(dc, rectTmp, getColumnLabel(colType));
}


wxRect GridData::drawColumnLabelBorder(wxDC& dc, const wxRect& rect) //returns remaining rectangle
{
    //draw white line
    {
        wxDCPenChanger dummy(dc, *wxWHITE_PEN);
        dc.DrawLine(rect.GetTopLeft(), rect.GetBottomLeft());
    }

    //draw border (with gradient)
    {
        wxDCPenChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_3DSHADOW));
        dc.GradientFillLinear(wxRect(rect.GetTopRight(), rect.GetBottomRight()), getColorLabelGradientFrom(), dc.GetPen().GetColour(), wxSOUTH);
        dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight() + wxPoint(1, 0));
    }

    return wxRect(rect.x + 1, rect.y, rect.width - 2, rect.height - 1); //we really don't like wxRect::Deflate, do we?
}


void GridData::drawColumnLabelBackground(wxDC& dc, const wxRect& rect, bool highlighted)
{
    if (highlighted)
        dc.GradientFillLinear(rect, getColorLabelGradientFocusFrom(), getColorLabelGradientFocusTo(), wxSOUTH);
    else //regular background gradient
        dc.GradientFillLinear(rect, getColorLabelGradientFrom(), getColorLabelGradientTo(), wxSOUTH); //clear overlapping cells
}


void GridData::drawColumnLabelText(wxDC& dc, const wxRect& rect, const std::wstring& text)
{
    wxDCTextColourChanger dummy(dc, getColorLabelText()); //accessibility: always set both foreground AND background colors!
    drawCellText(dc, rect, text, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
}

//----------------------------------------------------------------------------------------------------------------
/*
                  SubWindow
                     /|\
                      |
     -----------------------------------
    |           |            |          |
CornerWin  RowLabelWin  ColLabelWin  MainWin

*/
class Grid::SubWindow : public wxWindow
{
public:
    SubWindow(Grid& parent) :
        wxWindow(&parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxWANTS_CHARS | wxBORDER_NONE, wxPanelNameStr),
        parent_(parent)
    {
        Connect(wxEVT_PAINT, wxPaintEventHandler(SubWindow::onPaintEvent), nullptr, this);
        Connect(wxEVT_SIZE,  wxSizeEventHandler (SubWindow::onSizeEvent),  nullptr, this);
        //http://wiki.wxwidgets.org/Flicker-Free_Drawing
        Connect(wxEVT_ERASE_BACKGROUND, wxEraseEventHandler(SubWindow::onEraseBackGround), nullptr, this);

        //SetDoubleBuffered(true); slow as hell!

        SetBackgroundStyle(wxBG_STYLE_PAINT);

        Connect(wxEVT_SET_FOCUS,  wxFocusEventHandler(SubWindow::onFocus), nullptr, this);
        Connect(wxEVT_KILL_FOCUS, wxFocusEventHandler(SubWindow::onFocus), nullptr, this);
        Connect(wxEVT_CHILD_FOCUS, wxEventHandler(SubWindow::onChildFocus), nullptr, this);

        Connect(wxEVT_LEFT_DOWN,    wxMouseEventHandler(SubWindow::onMouseLeftDown  ), nullptr, this);
        Connect(wxEVT_LEFT_UP,      wxMouseEventHandler(SubWindow::onMouseLeftUp    ), nullptr, this);
        Connect(wxEVT_LEFT_DCLICK,  wxMouseEventHandler(SubWindow::onMouseLeftDouble), nullptr, this);
        Connect(wxEVT_RIGHT_DOWN,   wxMouseEventHandler(SubWindow::onMouseRightDown ), nullptr, this);
        Connect(wxEVT_RIGHT_UP,     wxMouseEventHandler(SubWindow::onMouseRightUp   ), nullptr, this);
        Connect(wxEVT_MOTION,       wxMouseEventHandler(SubWindow::onMouseMovement  ), nullptr, this);
        Connect(wxEVT_LEAVE_WINDOW, wxMouseEventHandler(SubWindow::onLeaveWindow    ), nullptr, this);
        Connect(wxEVT_MOUSEWHEEL,   wxMouseEventHandler(SubWindow::onMouseWheel     ), nullptr, this);
        Connect(wxEVT_MOUSE_CAPTURE_LOST, wxMouseCaptureLostEventHandler(SubWindow::onMouseCaptureLost), nullptr, this);

        Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(SubWindow::onKeyDown), nullptr, this);

        assert(GetClientAreaOrigin() == wxPoint()); //generally assumed when dealing with coordinates below
    }
    Grid& refParent() { return parent_; }
    const Grid& refParent() const { return parent_; }

    template <class T>
    bool sendEventNow(T&& event) //take both "rvalue + lvalues", return "true" if a suitable event handler function was found and executed, and the function did not call wxEvent::Skip.
    {
        if (wxEvtHandler* evtHandler = parent_.GetEventHandler())
            return evtHandler->ProcessEvent(event);
        return false;
    }

protected:
    void setToolTip(const std::wstring& text) //proper fix for wxWindow
    {
        wxToolTip* tt = GetToolTip();

        const wxString oldText = tt ? tt->GetTip() : wxString();
        if (text != oldText)
        {
            if (text.empty())
                SetToolTip(nullptr); //wxGTK doesn't allow wxToolTip with empty text!
            else
            {
                //wxWidgets bug: tooltip multiline property is defined by first tooltip text containing newlines or not (same is true for maximum width)
                if (!tt)
                    SetToolTip(new wxToolTip(L"a                                                                b\n\
                                                           a                                                                b")); //ugly, but working (on Windows)
                tt = GetToolTip(); //should be bound by now
                assert(tt);
                if (tt)
                    tt->SetTip(text);
            }
        }
    }

private:
    virtual void render(wxDC& dc, const wxRect& rect) = 0;

    virtual void onFocus(wxFocusEvent& event) { event.Skip(); }
    virtual void onChildFocus(wxEvent& event) {} //wxGTK::wxScrolledWindow automatically scrolls to child window when child gets focus -> prevent!

    virtual void onMouseLeftDown  (wxMouseEvent& event) { event.Skip(); }
    virtual void onMouseLeftUp    (wxMouseEvent& event) { event.Skip(); }
    virtual void onMouseLeftDouble(wxMouseEvent& event) { event.Skip(); }
    virtual void onMouseRightDown (wxMouseEvent& event) { event.Skip(); }
    virtual void onMouseRightUp   (wxMouseEvent& event) { event.Skip(); }
    virtual void onMouseMovement  (wxMouseEvent& event) { event.Skip(); }
    virtual void onLeaveWindow    (wxMouseEvent& event) { event.Skip(); }
    virtual void onMouseCaptureLost(wxMouseCaptureLostEvent& event) { event.Skip(); }

    void onKeyDown(wxKeyEvent& event)
    {
        if (!sendEventNow(event)) //let parent collect all key events
            event.Skip();
    }

    void onMouseWheel(wxMouseEvent& event)
    {
        /*
          MSDN, WM_MOUSEWHEEL: "Sent to the focus window when the mouse wheel is rotated.
          The DefWindowProc function propagates the message to the window's parent.
          There should be no internal forwarding of the message, since DefWindowProc propagates
          it up the parent chain until it finds a window that processes it."

          On OS X there is no such propagation! => we need a redirection (the same wxGrid implements)
         */

        //new wxWidgets 3.0 screw-up for GTK2: wxScrollHelperEvtHandler::ProcessEvent() ignores wxEVT_MOUSEWHEEL events
        //thereby breaking the scenario of redirection to parent we need here (but also breaking their very own wxGrid sample)
        //=> call wxScrolledWindow mouse wheel handler directly
        parent_.HandleOnMouseWheel(event);

        //if (!sendEventNow(event))
        //   event.Skip();
    }

    void onPaintEvent(wxPaintEvent& event)
    {
        //wxAutoBufferedPaintDC dc(this); -> this one happily fucks up for RTL layout by not drawing the first column (x = 0)!
        BufferedPaintDC dc(*this, doubleBuffer_);

        assert(GetSize() == GetClientSize());

        const wxRegion& updateReg = GetUpdateRegion();
        for (wxRegionIterator it = updateReg; it; ++it)
            render(dc, it.GetRect());
    }

    void onSizeEvent(wxSizeEvent& event)
    {
        Refresh();
        event.Skip();
    }

    void onEraseBackGround(wxEraseEvent& event) {}

    Grid& parent_;
    Opt<wxBitmap> doubleBuffer_;
};

//----------------------------------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------------------------------

class Grid::CornerWin : public SubWindow
{
public:
    CornerWin(Grid& parent) : SubWindow(parent) {}

private:
    bool AcceptsFocus() const override { return false; }

    void render(wxDC& dc, const wxRect& rect) override
    {
        const wxRect& clientRect = GetClientRect();

        dc.GradientFillLinear(clientRect, getColorLabelGradientFrom(), getColorLabelGradientTo(), wxSOUTH);

        dc.SetPen(wxSystemSettings::GetColour(wxSYS_COLOUR_3DSHADOW));

        {
            wxDCPenChanger dummy(dc, getColorLabelGradientFrom());
            dc.DrawLine(clientRect.GetTopLeft(), clientRect.GetTopRight());
        }

        dc.GradientFillLinear(wxRect(clientRect.GetBottomLeft (), clientRect.GetTopLeft ()), getColorLabelGradientFrom(), dc.GetPen().GetColour(), wxSOUTH);
        dc.GradientFillLinear(wxRect(clientRect.GetBottomRight(), clientRect.GetTopRight()), getColorLabelGradientFrom(), dc.GetPen().GetColour(), wxSOUTH);

        dc.DrawLine(clientRect.GetBottomLeft(), clientRect.GetBottomRight());

        wxRect rectShrinked = clientRect;
        rectShrinked.Deflate(1);
        dc.SetPen(*wxWHITE_PEN);

        //dc.DrawLine(clientRect.GetTopLeft(), clientRect.GetTopRight() + wxPoint(1, 0));
        dc.DrawLine(rectShrinked.GetTopLeft(), rectShrinked.GetBottomLeft() + wxPoint(0, 1));
    }
};

//----------------------------------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------------------------------

class Grid::RowLabelWin : public SubWindow
{
public:
    RowLabelWin(Grid& parent) :
        SubWindow(parent),
        rowHeight_(parent.GetCharHeight() + 2 + 1) {} //default height; don't call any functions on "parent" other than those from wxWindow during construction!
    //2 for some more space, 1 for bottom border (gives 15 + 2 + 1 on Windows, 17 + 2 + 1 on Ubuntu)

    int getBestWidth(ptrdiff_t rowFrom, ptrdiff_t rowTo)
    {
        wxClientDC dc(this);

        wxFont labelFont = GetFont();
        //labelFont.SetWeight(wxFONTWEIGHT_BOLD);
        dc.SetFont(labelFont); //harmonize with RowLabelWin::render()!

        int bestWidth = 0;
        for (ptrdiff_t i = rowFrom; i <= rowTo; ++i)
            bestWidth = std::max(bestWidth, dc.GetTextExtent(formatRow(i)).GetWidth() + 2 * ROW_LABEL_BORDER);
        return bestWidth;
    }

    size_t getLogicalHeight() const { return refParent().getRowCount() * rowHeight_; }

    ptrdiff_t getRowAtPos(ptrdiff_t posY) const //returns < 0 on invalid input, else row number within: [0, rowCount]; rowCount if out of range
    {
        if (posY >= 0 && rowHeight_ > 0)
        {
            const size_t row = posY / rowHeight_;
            return std::min(row, refParent().getRowCount());
        }
        return -1;
    }

    int getRowHeight() const { return rowHeight_; } //guarantees to return size >= 1 !
    void setRowHeight(int height) { assert(height > 0); rowHeight_ = std::max(1, height); }

    wxRect getRowLabelArea(size_t row) const //returns empty rect if row not found
    {
        assert(GetClientAreaOrigin() == wxPoint());
        if (row < refParent().getRowCount())
            return wxRect(wxPoint(0, rowHeight_ * row),
                          wxSize(GetClientSize().GetWidth(), rowHeight_));
        return wxRect();
    }

    std::pair<ptrdiff_t, ptrdiff_t> getRowsOnClient(const wxRect& clientRect) const //returns range [begin, end)
    {
        const int yFrom = refParent().CalcUnscrolledPosition(clientRect.GetTopLeft    ()).y;
        const int yTo   = refParent().CalcUnscrolledPosition(clientRect.GetBottomRight()).y;

        return std::make_pair(std::max(yFrom / rowHeight_, 0),
                              std::min<ptrdiff_t>((yTo  / rowHeight_) + 1, refParent().getRowCount()));
    }

private:
    static std::wstring formatRow(size_t row) { return toGuiString(row + 1); } //convert number to std::wstring including thousands separator

    bool AcceptsFocus() const override { return false; }

    void render(wxDC& dc, const wxRect& rect) override
    {
        /*
        IsEnabled() vs IsThisEnabled() since wxWidgets 2.9.5:

        void wxWindowBase::NotifyWindowOnEnableChange(), called from bool wxWindowBase::Enable(), fails to refresh
        child elements when disabling a IsTopLevel() dialog, e.g. when showing a modal dialog.
        The unfortunate effect on XP for using IsEnabled() when rendering the grid is that the user can move the modal dialog
        and *draw* with it on the background while the grid refreshes as disabled incrementally!

        => Don't use IsEnabled() since it considers the top level window. The brittle wxWidgets implementation is right in their intention,
        but wrong when not refreshing child-windows: the control designer decides how his control should be rendered!

        => IsThisEnabled() OTOH is too shallow and does not consider parent windows which are not top level.

        The perfect solution would be a bool ShouldBeDrawnActive() { return "IsEnabled() but ignore effects of showing a modal dialog"; }

        However "IsThisEnabled()" is good enough (same like the old IsEnabled() on wxWidgets 2.8.12) and it avoids this pathetic behavior on XP.
        (Similar problem on Win 7: e.g. directly click sync button without comparing first)
        */
        if (IsThisEnabled())
            clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
        else
            clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE));

        wxFont labelFont = GetFont();
        //labelFont.SetWeight(wxFONTWEIGHT_BOLD);
        dc.SetFont(labelFont); //harmonize with RowLabelWin::getBestWidth()!

        auto rowRange = getRowsOnClient(rect); //returns range [begin, end)
        for (auto row = rowRange.first; row < rowRange.second; ++row)
        {
            wxRect singleLabelArea = getRowLabelArea(row); //returns empty rect if row not found
            if (singleLabelArea.height > 0)
            {
                singleLabelArea.y = refParent().CalcScrolledPosition(singleLabelArea.GetTopLeft()).y;
                drawRowLabel(dc, singleLabelArea, row);
            }
        }
    }

    void drawRowLabel(wxDC& dc, const wxRect& rect, size_t row)
    {
        //clearArea(dc, rect, getColorRowLabel());
        dc.GradientFillLinear(rect, getColorLabelGradientFrom(), getColorLabelGradientTo(), wxEAST); //clear overlapping cells
        wxDCTextColourChanger dummy3(dc, getColorLabelText()); //accessibility: always set both foreground AND background colors!

        //label text
        wxRect textRect = rect;
        textRect.Deflate(1);

        GridData::drawCellText(dc, textRect, formatRow(row), wxALIGN_CENTRE);

        //border lines
        {
            wxDCPenChanger dummy(dc, *wxWHITE_PEN);
            dc.DrawLine(rect.GetTopLeft(), rect.GetTopRight());
        }
        {
            wxDCPenChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_3DSHADOW));
            dc.DrawLine(rect.GetTopLeft(),     rect.GetBottomLeft());
            dc.DrawLine(rect.GetBottomLeft(),  rect.GetBottomRight());
            dc.DrawLine(rect.GetBottomRight(), rect.GetTopRight() + wxPoint(0, -1));
        }
    }

    void onMouseLeftDown(wxMouseEvent& event) override { refParent().redirectRowLabelEvent(event); }
    void onMouseMovement(wxMouseEvent& event) override { refParent().redirectRowLabelEvent(event); }
    void onMouseLeftUp  (wxMouseEvent& event) override { refParent().redirectRowLabelEvent(event); }

    int rowHeight_;
};


namespace
{
class ColumnResizing
{
public:
    ColumnResizing(wxWindow& wnd, size_t col, int startWidth, int clientPosX) :
        wnd_(wnd), col_(col), startWidth_(startWidth), clientPosX_(clientPosX)
    {
        wnd_.CaptureMouse();
    }
    ~ColumnResizing()
    {
        if (wnd_.HasCapture())
            wnd_.ReleaseMouse();
    }

    size_t getColumn      () const { return col_; }
    int    getStartWidth  () const { return startWidth_; }
    int    getStartPosX   () const { return clientPosX_; }

private:
    wxWindow& wnd_;
    const size_t col_;
    const int    startWidth_;
    const int    clientPosX_;
};


class ColumnMove
{
public:
    ColumnMove(wxWindow& wnd, size_t colFrom, int clientPosX) :
        wnd_(wnd),
        colFrom_(colFrom),
        colTo_(colFrom),
        clientPosX_(clientPosX) { wnd_.CaptureMouse(); }
    ~ColumnMove() { if (wnd_.HasCapture()) wnd_.ReleaseMouse(); }

    size_t  getColumnFrom() const { return colFrom_; }
    size_t& refColumnTo() { return colTo_; }
    int     getStartPosX () const { return clientPosX_; }

    bool isRealMove() const { return !singleClick_; }
    void setRealMove() { singleClick_ = false; }

private:
    wxWindow& wnd_;
    const size_t colFrom_;
    size_t colTo_;
    const int clientPosX_;
    bool singleClick_ = true;
};
}

//----------------------------------------------------------------------------------------------------------------

class Grid::ColLabelWin : public SubWindow
{
public:
    ColLabelWin(Grid& parent) : SubWindow(parent) {}

private:
    bool AcceptsFocus() const override { return false; }

    void render(wxDC& dc, const wxRect& rect) override
    {
        if (IsThisEnabled())
            clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
        else
            clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE));

        //coordinate with "colLabelHeight" in Grid constructor:
        wxFont labelFont = GetFont();
        labelFont.SetWeight(wxFONTWEIGHT_BOLD);
        dc.SetFont(labelFont);

        wxDCTextColourChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels

        const int colLabelHeight = refParent().colLabelHeight_;

        wxPoint labelAreaTL(refParent().CalcScrolledPosition(wxPoint(0, 0)).x, 0); //client coordinates

        std::vector<ColumnWidth> absWidths = refParent().getColWidths(); //resolve stretched widths
        for (auto it = absWidths.begin(); it != absWidths.end(); ++it)
        {
            const size_t col = it - absWidths.begin();
            const int width  = it->width_; //don't use unsigned for calculations!

            if (labelAreaTL.x > rect.GetRight())
                return; //done, rect is fully covered
            if (labelAreaTL.x + width > rect.x)
                drawColumnLabel(dc, wxRect(labelAreaTL, wxSize(width, colLabelHeight)), col, it->type_);
            labelAreaTL.x += width;
        }
        if (labelAreaTL.x > rect.GetRight())
            return; //done, rect is fully covered

        //fill gap after columns and cover full width
        if (fillGapAfterColumns)
        {
            int totalWidth = 0;
            for (const ColumnWidth& cw : absWidths)
                totalWidth += cw.width_;
            const int clientWidth = GetClientSize().GetWidth(); //need reliable, stable width in contrast to rect.width

            if (totalWidth < clientWidth)
                drawColumnLabel(dc, wxRect(labelAreaTL, wxSize(clientWidth - totalWidth, colLabelHeight)), absWidths.size(), ColumnType::NONE);
        }
    }

    void drawColumnLabel(wxDC& dc, const wxRect& rect, size_t col, ColumnType colType)
    {
        if (auto dataView = refParent().getDataProvider())
        {
            const bool isHighlighted = activeResizing_    ? col == activeResizing_   ->getColumn    () : //highlight_ column on mouse-over
                                       activeClickOrMove_ ? col == activeClickOrMove_->getColumnFrom() :
                                       highlightCol_      ? col == *highlightCol_                      :
                                       false;

            RecursiveDcClipper clip(dc, rect);
            dataView->renderColumnLabel(refParent(), dc, rect, colType, isHighlighted);

            //draw move target location
            if (refParent().allowColumnMove_)
                if (activeClickOrMove_ && activeClickOrMove_->isRealMove())
                {
                    if (col + 1 == activeClickOrMove_->refColumnTo()) //handle pos 1, 2, .. up to "at end" position
                        dc.GradientFillLinear(wxRect(rect.GetTopRight(), rect.GetBottomRight() + wxPoint(-2, 0)), getColorLabelGradientFrom(), *wxBLUE, wxSOUTH);
                    else if (col == activeClickOrMove_->refColumnTo() && col == 0) //pos 0
                        dc.GradientFillLinear(wxRect(rect.GetTopLeft(), rect.GetBottomLeft() + wxPoint(2, 0)), getColorLabelGradientFrom(), *wxBLUE, wxSOUTH);
                }
        }
    }

    void onMouseLeftDown(wxMouseEvent& event) override
    {
        if (FindFocus() != &refParent().getMainWin())
            refParent().getMainWin().SetFocus();

        activeResizing_.reset();
        activeClickOrMove_.reset();

        if (Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition()))
        {
            if (action->wantResize)
            {
                if (!event.LeftDClick()) //double-clicks never seem to arrive here; why is this checked at all???
                    if (Opt<int> colWidth = refParent().getColWidth(action->col))
                        activeResizing_ = std::make_unique<ColumnResizing>(*this, action->col, *colWidth, event.GetPosition().x);
            }
            else //a move or single click
                activeClickOrMove_ = std::make_unique<ColumnMove>(*this, action->col, event.GetPosition().x);
        }
        event.Skip();
    }

    void onMouseLeftUp(wxMouseEvent& event) override
    {
        activeResizing_.reset(); //nothing else to do, actual work done by onMouseMovement()

        if (activeClickOrMove_)
        {
            if (activeClickOrMove_->isRealMove())
            {
                if (refParent().allowColumnMove_)
                {
                    const size_t colFrom = activeClickOrMove_->getColumnFrom();
                    size_t       colTo   = activeClickOrMove_->refColumnTo();

                    if (colTo > colFrom) //simulate "colFrom" deletion
                        --colTo;

                    refParent().moveColumn(colFrom, colTo);
                }
            }
            else //notify single label click
            {
                if (const Opt<ColumnType> colType = refParent().colToType(activeClickOrMove_->getColumnFrom()))
                    sendEventNow(GridLabelClickEvent(EVENT_GRID_COL_LABEL_MOUSE_LEFT, event, *colType));
            }
            activeClickOrMove_.reset();
        }

        refParent().updateWindowSizes(); //looks strange if done during onMouseMovement()
        refParent().Refresh();
        event.Skip();
    }

    void onMouseCaptureLost(wxMouseCaptureLostEvent& event) override
    {
        activeResizing_.reset();
        activeClickOrMove_.reset();
        Refresh();
        //event.Skip(); -> we DID handle it!
    }

    void onMouseLeftDouble(wxMouseEvent& event) override
    {
        if (Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition()))
            if (action->wantResize)
            {
                //auto-size visible range on double-click
                const int bestWidth = refParent().getBestColumnSize(action->col); //return -1 on error
                if (bestWidth >= 0)
                {
                    refParent().setColumnWidth(bestWidth, action->col, ALLOW_GRID_EVENT);
                    refParent().Refresh(); //refresh main grid as well!
                }
            }
        event.Skip();
    }

    void onMouseMovement(wxMouseEvent& event) override
    {
        if (activeResizing_)
        {
            const auto col     = activeResizing_->getColumn();
            const int newWidth = activeResizing_->getStartWidth() + event.GetPosition().x - activeResizing_->getStartPosX();

            //set width tentatively
            refParent().setColumnWidth(newWidth, col, ALLOW_GRID_EVENT);

            //check if there's a small gap after last column, if yes, fill it
            const int gapWidth = GetClientSize().GetWidth() - refParent().getColWidthsSum(GetClientSize().GetWidth());
            if (std::abs(gapWidth) < COLUMN_FILL_GAP_TOLERANCE)
                refParent().setColumnWidth(newWidth + gapWidth, col, ALLOW_GRID_EVENT);

            refParent().Refresh(); //refresh columns on main grid as well!
        }
        else if (activeClickOrMove_)
        {
            const int clientPosX = event.GetPosition().x;
            if (std::abs(clientPosX - activeClickOrMove_->getStartPosX()) > COLUMN_MOVE_DELAY) //real move (not a single click)
            {
                activeClickOrMove_->setRealMove();

                const ptrdiff_t col = refParent().clientPosToMoveTargetColumn(event.GetPosition());
                if (col >= 0)
                    activeClickOrMove_->refColumnTo() = col;
            }
        }
        else
        {
            if (const Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition()))
            {
                highlightCol_ = action->col;

                if (action->wantResize)
                    SetCursor(wxCURSOR_SIZEWE); //set window-local only! :)
                else
                    SetCursor(*wxSTANDARD_CURSOR);
            }
            else
            {
                highlightCol_ = NoValue();
                SetCursor(*wxSTANDARD_CURSOR);
            }
        }

        const std::wstring toolTip = [&]
        {
            const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition());
            const ColumnType colType = refParent().getColumnAtPos(absPos.x).colType; //returns ColumnType::NONE if no column at x position!
            if (colType != ColumnType::NONE)
                if (auto prov = refParent().getDataProvider())
                    return prov->getToolTip(colType);
            return std::wstring();
        }();
        setToolTip(toolTip);

        Refresh();
        event.Skip();
    }

    void onLeaveWindow(wxMouseEvent& event) override
    {
        highlightCol_ = NoValue(); //wxEVT_LEAVE_WINDOW does not respect mouse capture! -> however highlight_ is drawn unconditionally during move/resize!
        Refresh();
        event.Skip();
    }

    void onMouseRightDown(wxMouseEvent& event) override
    {
        if (const Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition()))
        {
            if (const Opt<ColumnType> colType = refParent().colToType(action->col))
                sendEventNow(GridLabelClickEvent(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, event, *colType)); //notify right click
            else assert(false);
        }
        else
            //notify right click (on free space after last column)
            if (fillGapAfterColumns)
                sendEventNow(GridLabelClickEvent(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, event, ColumnType::NONE));

        event.Skip();
    }

    std::unique_ptr<ColumnResizing> activeResizing_;
    std::unique_ptr<ColumnMove>     activeClickOrMove_;
    Opt<size_t>                     highlightCol_; //column during mouse-over
};

//----------------------------------------------------------------------------------------------------------------
namespace
{
const wxEventType EVENT_GRID_HAS_SCROLLED = wxNewEventType(); //internal to Grid::MainWin::ScrollWindow()
}
//----------------------------------------------------------------------------------------------------------------

class Grid::MainWin : public SubWindow
{
public:
    MainWin(Grid& parent,
            RowLabelWin& rowLabelWin,
            ColLabelWin& colLabelWin) : SubWindow(parent),
        rowLabelWin_(rowLabelWin),
        colLabelWin_(colLabelWin)
    {
        Connect(EVENT_GRID_HAS_SCROLLED, wxEventHandler(MainWin::onRequestWindowUpdate), nullptr, this);
    }

    ~MainWin() { assert(!gridUpdatePending_); }

    size_t getCursor() const { return cursorRow_; }
    size_t getAnchor() const { return selectionAnchor_; }

    void setCursor(size_t newCursorRow, size_t newAnchorRow)
    {
        cursorRow_       = newCursorRow;
        selectionAnchor_ = newAnchorRow;
        activeSelection_.reset(); //e.g. user might search with F3 while holding down left mouse button
    }

private:
    void render(wxDC& dc, const wxRect& rect) override
    {
        if (IsThisEnabled())
            clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
        else
            clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE));

        dc.SetFont(GetFont()); //harmonize with Grid::getBestColumnSize()

        wxDCTextColourChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels

        std::vector<ColumnWidth> absWidths = refParent().getColWidths(); //resolve stretched widths
        {
            int totalRowWidth = 0;
            for (const ColumnWidth& cw : absWidths)
                totalRowWidth += cw.width_;

            //fill gap after columns and cover full width
            if (fillGapAfterColumns)
                totalRowWidth = std::max(totalRowWidth, GetClientSize().GetWidth());

            if (auto prov = refParent().getDataProvider())
            {
                RecursiveDcClipper dummy2(dc, rect); //do NOT draw background on cells outside of invalidated rect invalidating foreground text!

                wxPoint cellAreaTL(refParent().CalcScrolledPosition(wxPoint(0, 0))); //client coordinates
                const int rowHeight = rowLabelWin_.getRowHeight();
                const auto rowRange = rowLabelWin_.getRowsOnClient(rect); //returns range [begin, end)

                //draw background lines
                for (auto row = rowRange.first; row < rowRange.second; ++row)
                {
                    const wxRect rowRect(cellAreaTL + wxPoint(0, row * rowHeight), wxSize(totalRowWidth, rowHeight));
                    RecursiveDcClipper dummy3(dc, rowRect);
                    prov->renderRowBackgound(dc, rowRect, row, refParent().IsThisEnabled(), drawAsSelected(row));
                }

                //draw single cells, column by column
                for (const ColumnWidth& cw : absWidths)
                {
                    if (cellAreaTL.x > rect.GetRight())
                        return; //done

                    if (cellAreaTL.x + cw.width_ > rect.x)
                        for (auto row = rowRange.first; row < rowRange.second; ++row)
                        {
                            const wxRect cellRect(cellAreaTL.x, cellAreaTL.y + row * rowHeight, cw.width_, rowHeight);
                            RecursiveDcClipper dummy3(dc, cellRect);
                            prov->renderCell(dc, cellRect, row, cw.type_, refParent().IsThisEnabled(), drawAsSelected(row), getRowHoverToDraw(row));
                        }
                    cellAreaTL.x += cw.width_;
                }
            }
        }
    }

    HoverArea getRowHoverToDraw(ptrdiff_t row) const
    {
        if (activeSelection_)
        {
            if (activeSelection_->getFirstClick().row_ == row)
                return activeSelection_->getFirstClick().hoverArea_;
        }
        else if (highlight_.row == row)
            return highlight_.rowHover;
        return HoverArea::NONE;
    }

    bool drawAsSelected(size_t row) const
    {
        if (activeSelection_) //check if user is currently selecting with mouse
        {
            const size_t rowFrom = std::min(activeSelection_->getStartRow(), activeSelection_->getCurrentRow());
            const size_t rowTo   = std::max(activeSelection_->getStartRow(), activeSelection_->getCurrentRow());

            if (rowFrom <= row && row <= rowTo)
                return activeSelection_->isPositiveSelect(); //overwrite default
        }
        return refParent().isSelected(row);
    }

    void onMouseLeftDown (wxMouseEvent& event) override { onMouseDown(event); }
    void onMouseLeftUp   (wxMouseEvent& event) override { onMouseUp  (event); }
    void onMouseRightDown(wxMouseEvent& event) override { onMouseDown(event); }
    void onMouseRightUp  (wxMouseEvent& event) override { onMouseUp  (event); }

    void onMouseLeftDouble(wxMouseEvent& event) override
    {
        if (auto prov = refParent().getDataProvider())
        {
            const wxPoint     absPos = refParent().CalcUnscrolledPosition(event.GetPosition());
            const ptrdiff_t      row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range
            const ColumnPosInfo  cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position!
            const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth);
            //client is interested in all double-clicks, even those outside of the grid!
            sendEventNow(GridClickEvent(EVENT_GRID_MOUSE_LEFT_DOUBLE, event, row, rowHover));
        }
        event.Skip();
    }

    void onMouseDown(wxMouseEvent& event) //handle left and right mouse button clicks (almost) the same
    {
        if (wxWindow::FindFocus() != this) //doesn't seem to happen automatically for right mouse button
            SetFocus();

        if (auto prov = refParent().getDataProvider())
        {
            const wxPoint    absPos = refParent().CalcUnscrolledPosition(event.GetPosition());
            const ptrdiff_t     row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range
            const ColumnPosInfo cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position!
            const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth);
            //row < 0 possible!!! Pressing "Menu key" simulates Mouse Right Down + Up at position 0xffff/0xffff!

            GridClickEvent mouseEvent(event.RightDown() ? EVENT_GRID_MOUSE_RIGHT_DOWN : EVENT_GRID_MOUSE_LEFT_DOWN, event, row, rowHover);

            if (row >= 0)
                if (!event.RightDown() || !refParent().isSelected(row)) //do NOT start a new selection if user right-clicks on a selected area!
                {
                    if (event.ControlDown())
                        activeSelection_ = std::make_unique<MouseSelection>(*this, row, !refParent().isSelected(row), mouseEvent);
                    else if (event.ShiftDown())
                    {
                        activeSelection_ = std::make_unique<MouseSelection>(*this, selectionAnchor_, true, mouseEvent);
                        refParent().clearSelection(ALLOW_GRID_EVENT);
                    }
                    else
                    {
                        activeSelection_ = std::make_unique<MouseSelection>(*this, row, true, mouseEvent);
                        refParent().clearSelection(ALLOW_GRID_EVENT);
                    }
                }
            //notify event *after* potential "clearSelection(true)" above: a client should first receive a GridRangeSelectEvent for clearing the grid, if necessary,
            //then GridClickEvent and the associated GridRangeSelectEvent one after the other
            sendEventNow(mouseEvent);

            Refresh();
        }
        event.Skip(); //allow changing focus
    }

    void onMouseUp(wxMouseEvent& event)
    {
        if (activeSelection_)
        {
            const size_t rowCount = refParent().getRowCount();
            if (rowCount > 0)
            {
                if (activeSelection_->getCurrentRow() < rowCount)
                {
                    cursorRow_ = activeSelection_->getCurrentRow();
                    selectionAnchor_ = activeSelection_->getStartRow(); //allowed to be "out of range"
                }
                else if (activeSelection_->getStartRow() < rowCount) //don't change cursor if "to" and "from" are out of range
                {
                    cursorRow_ = rowCount - 1;
                    selectionAnchor_ = activeSelection_->getStartRow(); //allowed to be "out of range"
                }
                else //total selection "out of range"
                    selectionAnchor_ = cursorRow_;
            }
            //slight deviation from Explorer: change cursor while dragging mouse! -> unify behavior with shift + direction keys

            refParent().selectRangeAndNotify(activeSelection_->getStartRow  (), //from
                                             activeSelection_->getCurrentRow(), //to
                                             activeSelection_->isPositiveSelect(),
                                             &activeSelection_->getFirstClick());
            activeSelection_.reset();
        }

        if (auto prov = refParent().getDataProvider())
        {
            //this one may point to row which is not in visible area!
            const wxPoint    absPos = refParent().CalcUnscrolledPosition(event.GetPosition());
            const ptrdiff_t     row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range
            const ColumnPosInfo cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position!
            const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth);
            //notify click event after the range selection! e.g. this makes sure the selection is applied before showing a context menu
            sendEventNow(GridClickEvent(event.RightUp() ? EVENT_GRID_MOUSE_RIGHT_UP : EVENT_GRID_MOUSE_LEFT_UP, event, row, rowHover));
        }

        //update highlight_ and tooltip: on OS X no mouse movement event is generated after a mouse button click (unlike on Windows)
        event.SetPosition(ScreenToClient(wxGetMousePosition())); //mouse position may have changed within above callbacks (e.g. context menu was shown)!
        onMouseMovement(event);

        Refresh();
        event.Skip(); //allow changing focus
    }

    void onMouseCaptureLost(wxMouseCaptureLostEvent& event) override
    {
        activeSelection_.reset();
        highlight_.row = -1;
        Refresh();
        //event.Skip(); -> we DID handle it!
    }

    void onMouseMovement(wxMouseEvent& event) override
    {
        if (auto prov = refParent().getDataProvider())
        {
            const ptrdiff_t rowCount = refParent().getRowCount();
            const wxPoint     absPos = refParent().CalcUnscrolledPosition(event.GetPosition());
            const ptrdiff_t      row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range
            const ColumnPosInfo  cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position!
            const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth);

            const std::wstring toolTip = [&]
            {
                if (cpi.colType != ColumnType::NONE && 0 <= row && row < rowCount)
                    return prov->getToolTip(row, cpi.colType);
                return std::wstring();
            }();
            setToolTip(toolTip); //show even during mouse selection!

            if (activeSelection_)
                activeSelection_->evalMousePos(); //call on both mouse movement + timer event!
            else
            {
                refreshHighlight(highlight_);
                highlight_.row      = row;
                highlight_.rowHover = rowHover;
                refreshHighlight(highlight_); //multiple Refresh() calls are condensed into single one!
            }
        }
        event.Skip();
    }

    void onLeaveWindow(wxMouseEvent& event) override //wxEVT_LEAVE_WINDOW does not respect mouse capture!
    {
        if (!activeSelection_)
        {
            refreshHighlight(highlight_);
            highlight_.row = -1;
        }

        event.Skip();
    }


    void onFocus(wxFocusEvent& event) override { Refresh(); event.Skip(); }

    class MouseSelection : private wxEvtHandler
    {
    public:
        MouseSelection(MainWin& wnd, size_t rowStart, bool positiveSelect, const GridClickEvent& firstClick) :
            wnd_(wnd), rowStart_(rowStart), rowCurrent_(rowStart), positiveSelect_(positiveSelect), firstClick_(firstClick)
        {
            wnd_.CaptureMouse();
            timer_.Connect(wxEVT_TIMER, wxEventHandler(MouseSelection::onTimer), nullptr, this);
            timer_.Start(100); //timer interval in ms
            evalMousePos();
        }
        ~MouseSelection() { if (wnd_.HasCapture()) wnd_.ReleaseMouse(); }

        size_t getStartRow     () const { return rowStart_;       }
        size_t getCurrentRow   () const { return rowCurrent_;     }
        bool   isPositiveSelect() const { return positiveSelect_; } //are we selecting or unselecting?
        const GridClickEvent& getFirstClick() const { return firstClick_; }

        void evalMousePos()
        {
            const auto now = std::chrono::steady_clock::now();
            const double deltaSecs = std::chrono::duration<double>(now - lastEvalTime_).count(); //unit: [sec]
            lastEvalTime_ = now;

            const wxPoint clientPos = wnd_.ScreenToClient(wxGetMousePosition());
            const wxSize clientSize = wnd_.GetClientSize();
            assert(wnd_.GetClientAreaOrigin() == wxPoint());

            //scroll while dragging mouse
            const int overlapPixY = clientPos.y < 0 ? clientPos.y :
                                    clientPos.y >= clientSize.GetHeight() ? clientPos.y - (clientSize.GetHeight() - 1) : 0;
            const int overlapPixX = clientPos.x < 0 ? clientPos.x :
                                    clientPos.x >= clientSize.GetWidth() ? clientPos.x - (clientSize.GetWidth() - 1) : 0;

            int pixelsPerUnitY = 0;
            wnd_.refParent().GetScrollPixelsPerUnit(nullptr, &pixelsPerUnitY);
            if (pixelsPerUnitY <= 0) return;

            const double mouseDragSpeedIncScrollU = pixelsPerUnitY > 0 ? MOUSE_DRAG_ACCELERATION * wnd_.rowLabelWin_.getRowHeight() / pixelsPerUnitY : 0; //unit: [scroll units / (pixel * sec)]

            auto autoScroll = [&](int overlapPix, double& toScroll)
            {
                if (overlapPix != 0)
                {
                    const double scrollSpeed = overlapPix * mouseDragSpeedIncScrollU; //unit: [scroll units / sec]
                    toScroll += scrollSpeed * deltaSecs;
                }
                else
                    toScroll = 0;
            };

            autoScroll(overlapPixX, toScrollX_);
            autoScroll(overlapPixY, toScrollY_);

            if (static_cast<int>(toScrollX_) != 0 || static_cast<int>(toScrollY_) != 0)
            {
                wnd_.refParent().scrollDelta(static_cast<int>(toScrollX_), static_cast<int>(toScrollY_)); //
                toScrollX_ -= static_cast<int>(toScrollX_); //rounds down for positive numbers, up for negative,
                toScrollY_ -= static_cast<int>(toScrollY_); //exactly what we want
            }

            //select current row *after* scrolling
            wxPoint clientPosTrimmed = clientPos;
            numeric::clamp(clientPosTrimmed.y, 0, clientSize.GetHeight() - 1); //do not select row outside client window!

            const wxPoint absPos = wnd_.refParent().CalcUnscrolledPosition(clientPosTrimmed);
            const ptrdiff_t newRow = wnd_.rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range
            if (newRow >= 0)
                if (rowCurrent_ != newRow)
                {
                    rowCurrent_ = newRow;
                    wnd_.Refresh();
                }
        }

    private:
        void onTimer(wxEvent& event) { evalMousePos(); }

        MainWin& wnd_;
        const size_t rowStart_;
        ptrdiff_t rowCurrent_;
        const bool positiveSelect_;
        const GridClickEvent firstClick_;
        wxTimer timer_;
        double toScrollX_ = 0; //count outstanding scroll unit fractions while dragging mouse
        double toScrollY_ = 0; //
        std::chrono::steady_clock::time_point lastEvalTime_ = std::chrono::steady_clock::now();
    };

    struct MouseHighlight
    {
        ptrdiff_t row = -1;
        HoverArea rowHover = HoverArea::NONE;
    };

    void ScrollWindow(int dx, int dy, const wxRect* rect) override
    {
        wxWindow::ScrollWindow(dx, dy, rect);
        rowLabelWin_.ScrollWindow(0, dy, rect);
        colLabelWin_.ScrollWindow(dx, 0, rect);

        //attention, wxGTK call sequence: wxScrolledWindow::Scroll() -> wxScrolledHelperNative::Scroll() -> wxScrolledHelperNative::DoScroll()
        //which *first* calls us, MainWin::ScrollWindow(), and *then* internally updates m_yScrollPosition
        //=> we cannot use CalcUnscrolledPosition() here which gives the wrong/outdated value!!!
        //=> we need to update asynchronously:
        //=> don't send async event repeatedly => severe performance issues on wxGTK!
        //=> can't use idle event neither: too few idle events on Windows, e.g. NO idle events while mouse drag-scrolling!
        //=> solution: send single async event at most!
        if (!gridUpdatePending_) //without guarding, the number of outstanding async events can become very high during scrolling!! test case: Ubuntu: 170; Windows: 20
        {
            gridUpdatePending_ = true;
            wxCommandEvent scrollEvent(EVENT_GRID_HAS_SCROLLED);
            AddPendingEvent(scrollEvent); //asynchronously call updateAfterScroll()
        }
    }

    void onRequestWindowUpdate(wxEvent& event)
    {
        assert(gridUpdatePending_);
        ZEN_ON_SCOPE_EXIT(gridUpdatePending_ = false);

        refParent().updateWindowSizes(false); //row label width has changed -> do *not* update scrollbars: recursion on wxGTK! -> still a problem, now that we're called async??
        rowLabelWin_.Update(); //update while dragging scroll thumb
    }

    void refreshRow(size_t row)
    {
        const wxRect& rowArea = rowLabelWin_.getRowLabelArea(row); //returns empty rect if row not found
        const wxPoint topLeft = refParent().CalcScrolledPosition(wxPoint(0, rowArea.y)); //absolute -> client coordinates
        wxRect cellArea(topLeft, wxSize(refParent().getColWidthsSum(GetClientSize().GetWidth()), rowArea.height));
        RefreshRect(cellArea, false);
    }

    void refreshHighlight(const MouseHighlight& hl)
    {
        const ptrdiff_t rowCount = refParent().getRowCount();
        if (0 <= hl.row && hl.row < rowCount && hl.rowHover != HoverArea::NONE) //no highlight_? => NOP!
            refreshRow(hl.row);
    }

    RowLabelWin& rowLabelWin_;
    ColLabelWin& colLabelWin_;

    std::unique_ptr<MouseSelection> activeSelection_; //bound while user is selecting with mouse
    MouseHighlight highlight_; //current mouse highlight_ (superseeded by activeSelection_ if available)

    ptrdiff_t cursorRow_ = 0;
    size_t selectionAnchor_ = 0;
    bool gridUpdatePending_ = false;
};

//----------------------------------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------------------------------

Grid::Grid(wxWindow* parent,
           wxWindowID id,
           const wxPoint& pos,
           const wxSize& size,
           long style,
           const wxString& name) : wxScrolledWindow(parent, id, pos, size, style | wxWANTS_CHARS, name)
{
    cornerWin_   = new CornerWin  (*this); //
    rowLabelWin_ = new RowLabelWin(*this); //owership handled by "this"
    colLabelWin_ = new ColLabelWin(*this); //
    mainWin_     = new MainWin    (*this, *rowLabelWin_, *colLabelWin_); //

    colLabelHeight_ = 2 * DEFAULT_COL_LABEL_BORDER + [&]() -> int
    {
        //coordinate with ColLabelWin::render():
        wxFont labelFont = colLabelWin_->GetFont();
        labelFont.SetWeight(wxFONTWEIGHT_BOLD);
        return labelFont.GetPixelSize().GetHeight();
    }();

    SetTargetWindow(mainWin_);

    SetInitialSize(size); //"Most controls will use this to set their initial size" -> why not

    assert(GetClientSize() == GetSize()); //borders are NOT allowed for Grid
    //reason: updateWindowSizes() wants to use "GetSize()" as a "GetClientSize()" including scrollbars

    Connect(wxEVT_PAINT,            wxPaintEventHandler(Grid::onPaintEvent     ), nullptr, this);
    Connect(wxEVT_ERASE_BACKGROUND, wxEraseEventHandler(Grid::onEraseBackGround), nullptr, this);
    Connect(wxEVT_SIZE,             wxSizeEventHandler (Grid::onSizeEvent      ), nullptr, this);

    Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(Grid::onKeyDown), nullptr, this);
}


void Grid::updateWindowSizes(bool updateScrollbar)
{
    /* We have to deal with TWO nasty circular dependencies:
    1.
        rowLabelWidth
            /|\
        mainWin::client width
            /|\
        SetScrollbars -> show/hide horizontal scrollbar depending on client width
            /|\
        mainWin::client height -> possibly trimmed by horizontal scrollbars
            /|\
        rowLabelWidth

    2.
        mainWin_->GetClientSize()
            /|\
        SetScrollbars -> show/hide scrollbars depending on whether client size is big enough
            /|\
        GetClientSize(); -> possibly trimmed by scrollbars
            /|\
        mainWin_->GetClientSize()  -> also trimmed, since it's a sub-window!
    */

    //break this vicious circle:

    //harmonize with Grid::GetSizeAvailableForScrollTarget()!

    //1. calculate row label width independent from scrollbars
    const int mainWinHeightGross = std::max(GetSize().GetHeight() - colLabelHeight_, 0); //independent from client sizes and scrollbars!
    const ptrdiff_t logicalHeight = rowLabelWin_->getLogicalHeight();                   //

    int rowLabelWidth = 0;
    if (drawRowLabel_ && logicalHeight > 0)
    {
        ptrdiff_t yFrom = CalcUnscrolledPosition(wxPoint(0, 0)).y;
        ptrdiff_t yTo   = CalcUnscrolledPosition(wxPoint(0, mainWinHeightGross - 1)).y ;
        numeric::clamp<ptrdiff_t>(yFrom, 0, logicalHeight - 1);
        numeric::clamp<ptrdiff_t>(yTo,   0, logicalHeight - 1);

        const ptrdiff_t rowFrom = rowLabelWin_->getRowAtPos(yFrom);
        const ptrdiff_t rowTo   = rowLabelWin_->getRowAtPos(yTo);
        if (rowFrom >= 0 && rowTo >= 0)
            rowLabelWidth = rowLabelWin_->getBestWidth(rowFrom, rowTo);
    }

    auto getMainWinSize = [&](const wxSize& clientSize) { return wxSize(std::max(0, clientSize.GetWidth() - rowLabelWidth), std::max(0, clientSize.GetHeight() - colLabelHeight_)); };

    auto setScrollbars2 = [&](int logWidth, int logHeight) //replace SetScrollbars, which loses precision of pixelsPerUnitX for some brain-dead reason
    {
        mainWin_->SetVirtualSize(logWidth, logHeight); //set before calling SetScrollRate():
        //else SetScrollRate() would fail to preserve scroll position when "new virtual pixel-pos > old virtual height"

        int ppsuX = 0; //pixel per scroll unit
        int ppsuY = 0;
        GetScrollPixelsPerUnit(&ppsuX, &ppsuY);

        const int ppsuNew = rowLabelWin_->getRowHeight();
        if (ppsuX != ppsuNew || ppsuY != ppsuNew) //support polling!
            SetScrollRate(ppsuNew, ppsuNew); //internally calls AdjustScrollbars() and GetVirtualSize()!

        AdjustScrollbars(); //lousy wxWidgets design decision: internally calls mainWin_->GetClientSize() without considering impact of scrollbars!
        //Attention: setting scrollbars triggers *synchronous* resize event if scrollbars are shown or hidden! => updateWindowSizes() recursion! (Windows)
    };

    //2. update managed windows' sizes: just assume scrollbars are already set correctly, even if they may not be (yet)!
    //this ensures mainWin_->SetVirtualSize() and AdjustScrollbars() are working with the correct main window size, unless sb change later, which triggers a recalculation anyway!
    const wxSize mainWinSize = getMainWinSize(GetClientSize());

    cornerWin_  ->SetSize(0, 0, rowLabelWidth, colLabelHeight_);
    rowLabelWin_->SetSize(0, colLabelHeight_, rowLabelWidth, mainWinSize.GetHeight());
    colLabelWin_->SetSize(rowLabelWidth, 0, mainWinSize.GetWidth(), colLabelHeight_);
    mainWin_    ->SetSize(rowLabelWidth, colLabelHeight_, mainWinSize.GetWidth(), mainWinSize.GetHeight());

    //avoid flicker in wxWindowMSW::HandleSize() when calling ::EndDeferWindowPos() where the sub-windows are moved only although they need to be redrawn!
    colLabelWin_->Refresh();
    mainWin_    ->Refresh();

    //3. update scrollbars: "guide wxScrolledHelper to not screw up too much"
    if (updateScrollbar)
    {
        const int mainWinWidthGross = getMainWinSize(GetSize()).GetWidth();

        if (logicalHeight <= mainWinHeightGross &&
            getColWidthsSum(mainWinWidthGross) <= mainWinWidthGross &&
            //this special case needs to be considered *only* when both scrollbars are flexible:
            showScrollbarX_ == SB_SHOW_AUTOMATIC &&
            showScrollbarY_ == SB_SHOW_AUTOMATIC)
            setScrollbars2(0, 0); //no scrollbars required at all! -> wxScrolledWindow requires active help to detect this special case!
        else
        {
            const int logicalWidthTmp = getColWidthsSum(mainWinSize.GetWidth()); //assuming vertical scrollbar stays as it is...
            setScrollbars2(logicalWidthTmp, logicalHeight); //if scrollbars are shown or hidden a new resize event recurses into updateWindowSizes()
            /*
            is there a risk of endless recursion? No, 2-level recursion at most, consider the following 6 cases:

            <----------gw---------->
            <----------nw------>
            ------------------------  /|\   /|\
            |                   |  |   |     |
            |     main window   |  |   nh    |
            |                   |  |   |     gh
            ------------------------  \|/    |
            |                   |  |         |
            ------------------------        \|/
                gw := gross width
                nw := net width := gross width - sb size
                gh := gross height
                nh := net height := gross height - sb size

            There are 6 cases that can occur:
            ---------------------------------
                lw := logical width
                lh := logical height

            1. lw <= gw && lh <= gh  => no scrollbars needed

            2. lw > gw  && lh > gh   => need both scrollbars

            3. lh > gh
                4.1 lw <= nw         => need vertical scrollbar only
                4.2 nw < lw <= gw    => need both scrollbars

            4. lw > gw
                3.1 lh <= nh         => need horizontal scrollbar only
                3.2 nh < lh <= gh    => need both scrollbars
            */
        }
    }
}


wxSize Grid::GetSizeAvailableForScrollTarget(const wxSize& size)
{
    //harmonize with Grid::updateWindowSizes()!

    //1. calculate row label width independent from scrollbars
    const int mainWinHeightGross = std::max(size.GetHeight() - colLabelHeight_, 0); //independent from client sizes and scrollbars!
    const ptrdiff_t logicalHeight = rowLabelWin_->getLogicalHeight();              //

    int rowLabelWidth = 0;
    if (drawRowLabel_ && logicalHeight > 0)
    {
        ptrdiff_t yFrom = CalcUnscrolledPosition(wxPoint(0, 0)).y;
        ptrdiff_t yTo   = CalcUnscrolledPosition(wxPoint(0, mainWinHeightGross - 1)).y ;
        numeric::clamp<ptrdiff_t>(yFrom, 0, logicalHeight - 1);
        numeric::clamp<ptrdiff_t>(yTo,   0, logicalHeight - 1);

        const ptrdiff_t rowFrom = rowLabelWin_->getRowAtPos(yFrom);
        const ptrdiff_t rowTo   = rowLabelWin_->getRowAtPos(yTo);
        if (rowFrom >= 0 && rowTo >= 0)
            rowLabelWidth = rowLabelWin_->getBestWidth(rowFrom, rowTo);
    }

    return size - wxSize(rowLabelWidth, colLabelHeight_);
}


void Grid::onPaintEvent(wxPaintEvent& event) { wxPaintDC dc(this); }


void Grid::onKeyDown(wxKeyEvent& event)
{
    int keyCode = event.GetKeyCode();
    if (GetLayoutDirection() == wxLayout_RightToLeft)
    {
        if (keyCode == WXK_LEFT || keyCode == WXK_NUMPAD_LEFT)
            keyCode = WXK_RIGHT;
        else if (keyCode == WXK_RIGHT || keyCode == WXK_NUMPAD_RIGHT)
            keyCode = WXK_LEFT;
    }

    const ptrdiff_t rowCount  = getRowCount();
    const ptrdiff_t cursorRow = mainWin_->getCursor();

    auto moveCursorTo = [&](ptrdiff_t row)
    {
        if (rowCount > 0)
        {
            numeric::clamp<ptrdiff_t>(row, 0, rowCount - 1);
            setGridCursor(row);
        }
    };

    auto selectWithCursorTo = [&](ptrdiff_t row)
    {
        if (rowCount > 0)
        {
            numeric::clamp<ptrdiff_t>(row, 0, rowCount - 1);
            selectWithCursor(row);
        }
    };

    switch (keyCode)
    {
        //case WXK_TAB:
        //    if (Navigate(event.ShiftDown() ? wxNavigationKeyEvent::IsBackward : wxNavigationKeyEvent::IsForward))
        //        return;
        //    break;

        case WXK_UP:
        case WXK_NUMPAD_UP:
            if (event.ShiftDown())
                selectWithCursorTo(cursorRow - 1);
            else if (event.ControlDown())
                scrollDelta(0, -1);
            else
                moveCursorTo(cursorRow - 1);
            return; //swallow event: wxScrolledWindow, wxWidgets 2.9.3 on Kubuntu x64 processes arrow keys: prevent this!

        case WXK_DOWN:
        case WXK_NUMPAD_DOWN:
            if (event.ShiftDown())
                selectWithCursorTo(cursorRow + 1);
            else if (event.ControlDown())
                scrollDelta(0, 1);
            else
                moveCursorTo(cursorRow + 1);
            return; //swallow event

        case WXK_LEFT:
        case WXK_NUMPAD_LEFT:
            if (event.ControlDown())
                scrollDelta(-1, 0);
            else if (event.ShiftDown())
                ;
            else
                moveCursorTo(cursorRow);
            return;

        case WXK_RIGHT:
        case WXK_NUMPAD_RIGHT:
            if (event.ControlDown())
                scrollDelta(1, 0);
            else if (event.ShiftDown())
                ;
            else
                moveCursorTo(cursorRow);
            return;

        case WXK_HOME:
        case WXK_NUMPAD_HOME:
            if (event.ShiftDown())
                selectWithCursorTo(0);
            //else if (event.ControlDown())
            //    ;
            else
                moveCursorTo(0);
            return;

        case WXK_END:
        case WXK_NUMPAD_END:
            if (event.ShiftDown())
                selectWithCursorTo(rowCount - 1);
            //else if (event.ControlDown())
            //    ;
            else
                moveCursorTo(rowCount - 1);
            return;

        case WXK_PAGEUP:
        case WXK_NUMPAD_PAGEUP:
            if (event.ShiftDown())
                selectWithCursorTo(cursorRow - GetClientSize().GetHeight() / rowLabelWin_->getRowHeight());
            //else if (event.ControlDown())
            //    ;
            else
                moveCursorTo(cursorRow - GetClientSize().GetHeight() / rowLabelWin_->getRowHeight());
            return;

        case WXK_PAGEDOWN:
        case WXK_NUMPAD_PAGEDOWN:
            if (event.ShiftDown())
                selectWithCursorTo(cursorRow + GetClientSize().GetHeight() / rowLabelWin_->getRowHeight());
            //else if (event.ControlDown())
            //    ;
            else
                moveCursorTo(cursorRow + GetClientSize().GetHeight() / rowLabelWin_->getRowHeight());
            return;

        case 'A':  //Ctrl + A - select all
            if (event.ControlDown())
                selectRangeAndNotify(0, rowCount, true /*positive*/, nullptr /*mouseInitiated*/);
            break;

        case WXK_NUMPAD_ADD: //CTRL + '+' - auto-size all
            if (event.ControlDown())
                autoSizeColumns(ALLOW_GRID_EVENT);
            return;
    }

    event.Skip();
}


void Grid::setColumnLabelHeight(int height)
{
    colLabelHeight_ = std::max(0, height);
    updateWindowSizes();
}


void Grid::showRowLabel(bool show)
{
    drawRowLabel_ = show;
    updateWindowSizes();
}


void Grid::selectAllRows(GridEventPolicy rangeEventPolicy)
{
    selection_.selectAll();
    mainWin_->Refresh();

    if (rangeEventPolicy == ALLOW_GRID_EVENT) //notify event, even if we're not triggered by user interaction
    {
        GridRangeSelectEvent selEvent(0, getRowCount(), true, nullptr);
        if (wxEvtHandler* evtHandler = GetEventHandler())
            evtHandler->ProcessEvent(selEvent);
    }
}


void Grid::clearSelection(GridEventPolicy rangeEventPolicy)
{
    selection_.clear();
    mainWin_->Refresh();

    if (rangeEventPolicy == ALLOW_GRID_EVENT) //notify event, even if we're not triggered by user interaction
    {
        GridRangeSelectEvent unselectionEvent(0, getRowCount(), false, nullptr);
        if (wxEvtHandler* evtHandler = GetEventHandler())
            evtHandler->ProcessEvent(unselectionEvent);
    }
}


void Grid::scrollDelta(int deltaX, int deltaY)
{
    int scrollPosX = 0;
    int scrollPosY = 0;
    GetViewStart(&scrollPosX, &scrollPosY);

    scrollPosX += deltaX;
    scrollPosY += deltaY;

    scrollPosX = std::max(0, scrollPosX); //wxScrollHelper::Scroll() will exit prematurely if input happens to be "-1"!
    scrollPosY = std::max(0, scrollPosY); //

    Scroll(scrollPosX, scrollPosY); //internally calls wxWindows::Update()!
    updateWindowSizes(); //may show horizontal scroll bar
}


void Grid::redirectRowLabelEvent(wxMouseEvent& event)
{
    event.m_x = 0;
    if (wxEvtHandler* evtHandler = mainWin_->GetEventHandler())
        evtHandler->ProcessEvent(event);

    if (event.ButtonDown() && wxWindow::FindFocus() != mainWin_)
        mainWin_->SetFocus();
}


size_t Grid::getRowCount() const
{
    return dataView_ ? dataView_->getRowCount() : 0;
}


void Grid::Refresh(bool eraseBackground, const wxRect* rect)
{
    const size_t rowCountNew = getRowCount();
    if (rowCountOld_ != rowCountNew)
    {
        rowCountOld_ = rowCountNew;
        updateWindowSizes();
    }

    if (selection_.maxSize() != rowCountNew) //clear selection only when needed (consider setSelectedRows())
        selection_.init(rowCountNew);

    wxScrolledWindow::Refresh(eraseBackground, rect);
}


void Grid::setRowHeight(int height)
{
    rowLabelWin_->setRowHeight(height);
    updateWindowSizes();
    Refresh();
}


void Grid::setColumnConfig(const std::vector<Grid::ColumnAttribute>& attr)
{
    //hold ownership of non-visible columns
    oldColAttributes_ = attr;

    std::vector<VisibleColumn> visCols;
    for (const ColumnAttribute& ca : attr)
    {
        assert(ca.type_ != ColumnType::NONE);
        if (ca.visible_)
            visCols.emplace_back(ca.type_, ca.offset_, ca.stretch_);
    }

    //"ownership" of visible columns is now within Grid
    visibleCols_ = visCols;

    updateWindowSizes();
    Refresh();
}


std::vector<Grid::ColumnAttribute> Grid::getColumnConfig() const
{
    //get non-visible columns (+ outdated visible ones)
    std::vector<ColumnAttribute> output = oldColAttributes_;

    auto iterVcols    = visibleCols_.begin();
    auto iterVcolsend = visibleCols_.end();

    //update visible columns but keep order of non-visible ones!
    for (ColumnAttribute& ca : output)
        if (ca.visible_)
        {
            if (iterVcols != iterVcolsend)
            {
                ca.type_    = iterVcols->type_;
                ca.stretch_ = iterVcols->stretch_;
                ca.offset_  = iterVcols->offset_;
                ++iterVcols;
            }
            else
                assert(false);
        }
    assert(iterVcols == iterVcolsend);

    return output;
}


void Grid::showScrollBars(Grid::ScrollBarStatus horizontal, Grid::ScrollBarStatus vertical)
{
    if (showScrollbarX_ == horizontal &&
        showScrollbarY_ == vertical) return; //support polling!

    showScrollbarX_ = horizontal;
    showScrollbarY_ = vertical;

    //the following wxGTK approach is pretty much identical to wxWidgets 2.9 ShowScrollbars() code!

    auto mapStatus = [](ScrollBarStatus sbStatus) -> GtkPolicyType
    {
        switch (sbStatus)
        {
            case SB_SHOW_AUTOMATIC:
                return GTK_POLICY_AUTOMATIC;
            case SB_SHOW_ALWAYS:
                return GTK_POLICY_ALWAYS;
            case SB_SHOW_NEVER:
                return GTK_POLICY_NEVER;
        }
        assert(false);
        return GTK_POLICY_AUTOMATIC;
    };

    GtkWidget* gridWidget = wxWindow::m_widget;
    GtkScrolledWindow* scrolledWindow = GTK_SCROLLED_WINDOW(gridWidget);
    ::gtk_scrolled_window_set_policy(scrolledWindow,
                                     mapStatus(horizontal),
                                     mapStatus(vertical));

    updateWindowSizes();
}



wxWindow& Grid::getCornerWin  () { return *cornerWin_;   }
wxWindow& Grid::getRowLabelWin() { return *rowLabelWin_; }
wxWindow& Grid::getColLabelWin() { return *colLabelWin_; }
wxWindow& Grid::getMainWin    () { return *mainWin_;     }
const wxWindow& Grid::getMainWin() const { return *mainWin_; }


Opt<Grid::ColAction> Grid::clientPosToColumnAction(const wxPoint& pos) const
{
    const int absPosX = CalcUnscrolledPosition(pos).x;
    if (absPosX >= 0)
    {
        const int resizeTolerance = allowColumnResize_ ? COLUMN_RESIZE_TOLERANCE : 0;
        std::vector<ColumnWidth> absWidths = getColWidths(); //resolve stretched widths

        int accuWidth = 0;
        for (size_t col = 0; col < absWidths.size(); ++col)
        {
            accuWidth += absWidths[col].width_;
            if (std::abs(absPosX - accuWidth) < resizeTolerance)
            {
                ColAction out;
                out.wantResize = true;
                out.col        = col;
                return out;
            }
            else if (absPosX < accuWidth)
            {
                ColAction out;
                out.wantResize = false;
                out.col        = col;
                return out;
            }
        }
    }
    return NoValue();
}


void Grid::moveColumn(size_t colFrom, size_t colTo)
{
    if (colFrom < visibleCols_.size() &&
        colTo   < visibleCols_.size() &&
        colTo != colFrom)
    {
        const VisibleColumn colAtt = visibleCols_[colFrom];
        visibleCols_.erase (visibleCols_.begin() + colFrom);
        visibleCols_.insert(visibleCols_.begin() + colTo, colAtt);
    }
}


ptrdiff_t Grid::clientPosToMoveTargetColumn(const wxPoint& pos) const
{

    const int absPosX = CalcUnscrolledPosition(pos).x;

    int accWidth = 0;
    std::vector<ColumnWidth> absWidths = getColWidths(); //resolve negative/stretched widths
    for (auto itCol = absWidths.begin(); itCol != absWidths.end(); ++itCol)
    {
        const int width = itCol->width_; //beware dreaded unsigned conversions!
        accWidth += width;

        if (absPosX < accWidth - width / 2)
            return itCol - absWidths.begin();
    }
    return absWidths.size();
}


ColumnType Grid::colToType(size_t col) const
{
    if (col < visibleCols_.size())
        return visibleCols_[col].type_;
    return ColumnType::NONE;
}


ptrdiff_t Grid::getRowAtPos(int posY) const { return rowLabelWin_->getRowAtPos(posY); }


Grid::ColumnPosInfo Grid::getColumnAtPos(int posX) const
{
    if (posX >= 0)
    {
        int accWidth = 0;
        for (const ColumnWidth& cw : getColWidths())
        {
            accWidth += cw.width_;
            if (posX < accWidth)
                return { cw.type_, posX + cw.width_ - accWidth, cw.width_ };
        }
    }
    return { ColumnType::NONE, 0, 0 };
}


wxRect Grid::getColumnLabelArea(ColumnType colType) const
{
    std::vector<ColumnWidth> absWidths = getColWidths(); //resolve negative/stretched widths

    //colType is not unique in general, but *this* function expects it!
    assert(std::count_if(absWidths.begin(), absWidths.end(), [&](const ColumnWidth& cw) { return cw.type_ == colType; }) <= 1);

    auto itCol = std::find_if(absWidths.begin(), absWidths.end(), [&](const ColumnWidth& cw) { return cw.type_ == colType; });
    if (itCol != absWidths.end())
    {
        ptrdiff_t posX = 0;
        for (auto it = absWidths.begin(); it != itCol; ++it)
            posX += it->width_;

        return wxRect(wxPoint(posX, 0), wxSize(itCol->width_, colLabelHeight_));
    }
    return wxRect();
}


void Grid::refreshCell(size_t row, ColumnType colType)
{
    const wxRect& colArea = getColumnLabelArea(colType); //returns empty rect if column not found
    const wxRect& rowArea = rowLabelWin_->getRowLabelArea(row); //returns empty rect if row not found
    if (colArea.height > 0 && rowArea.height > 0)
    {
        const wxPoint topLeft = CalcScrolledPosition(wxPoint(colArea.x, rowArea.y)); //absolute -> client coordinates
        const wxRect cellArea(topLeft, wxSize(colArea.width, rowArea.height));

        getMainWin().RefreshRect(cellArea, false);
    }
}


void Grid::setGridCursor(size_t row)
{
    mainWin_->setCursor(row, row);
    makeRowVisible(row);

    selection_.clear(); //clear selection, do NOT fire event
    selectRangeAndNotify(row, row, true /*positive*/, nullptr /*mouseInitiated*/); //set new selection + fire event

    mainWin_->Refresh();
    rowLabelWin_->Refresh(); //row labels! (Kubuntu)
}


void Grid::selectWithCursor(ptrdiff_t row)
{
    const size_t anchorRow = mainWin_->getAnchor();

    mainWin_->setCursor(row, anchorRow);
    makeRowVisible(row);

    selection_.clear(); //clear selection, do NOT fire event
    selectRangeAndNotify(anchorRow, row, true /*positive*/, nullptr /*mouseInitiated*/); //set new selection + fire event

    mainWin_->Refresh();
    rowLabelWin_->Refresh();
}


void Grid::makeRowVisible(size_t row)
{
    const wxRect labelRect = rowLabelWin_->getRowLabelArea(row); //returns empty rect if row not found
    if (labelRect.height > 0)
    {
        int scrollPosX = 0;
        GetViewStart(&scrollPosX, nullptr);

        int pixelsPerUnitY = 0;
        GetScrollPixelsPerUnit(nullptr, &pixelsPerUnitY);
        if (pixelsPerUnitY <= 0) return;

        const int clientPosY = CalcScrolledPosition(labelRect.GetTopLeft()).y;
        if (clientPosY < 0)
        {
            const int scrollPosY = labelRect.y / pixelsPerUnitY;
            Scroll(scrollPosX, scrollPosY); //internally calls wxWindows::Update()!
            updateWindowSizes(); //may show horizontal scroll bar
        }
        else if (clientPosY + labelRect.height > rowLabelWin_->GetClientSize().GetHeight())
        {
            auto execScroll = [&](int clientHeight)
            {
                const int scrollPosY = std::ceil((labelRect.y - clientHeight +
                                                  labelRect.height) / static_cast<double>(pixelsPerUnitY));
                Scroll(scrollPosX, scrollPosY);
                updateWindowSizes(); //may show horizontal scroll bar
            };

            const int clientHeightBefore = rowLabelWin_->GetClientSize().GetHeight();
            execScroll(clientHeightBefore);

            //client height may decrease after scroll due to a new horizontal scrollbar, resulting in a partially visible last row
            const int clientHeightAfter = rowLabelWin_->GetClientSize().GetHeight();
            if (clientHeightAfter < clientHeightBefore)
                execScroll(clientHeightAfter);
        }
    }
}


void Grid::selectRangeAndNotify(ptrdiff_t rowFrom, ptrdiff_t rowTo, bool positive, const GridClickEvent* mouseInitiated)
{
    //sort + convert to half-open range
    auto rowFirst = std::min(rowFrom, rowTo);
    auto rowLast  = std::max(rowFrom, rowTo) + 1;

    const size_t rowCount = getRowCount();
    numeric::clamp<ptrdiff_t>(rowFirst, 0, rowCount);
    numeric::clamp<ptrdiff_t>(rowLast,  0, rowCount);

    selection_.selectRange(rowFirst, rowLast, positive);

    //notify event
    GridRangeSelectEvent selectionEvent(rowFirst, rowLast, positive, mouseInitiated);
    if (wxEvtHandler* evtHandler = GetEventHandler())
        evtHandler->ProcessEvent(selectionEvent);

    mainWin_->Refresh();
}


void Grid::scrollTo(size_t row)
{
    const wxRect labelRect = rowLabelWin_->getRowLabelArea(row); //returns empty rect if row not found
    if (labelRect.height > 0)
    {
        int pixelsPerUnitY = 0;
        GetScrollPixelsPerUnit(nullptr, &pixelsPerUnitY);
        if (pixelsPerUnitY > 0)
        {
            const int scrollPosYNew = labelRect.y / pixelsPerUnitY;
            int scrollPosXOld = 0;
            int scrollPosYOld = 0;
            GetViewStart(&scrollPosXOld, &scrollPosYOld);

            if (scrollPosYOld != scrollPosYNew) //support polling
            {
                Scroll(scrollPosXOld, scrollPosYNew); //internally calls wxWindows::Update()!
                updateWindowSizes(); //may show horizontal scroll bar
                Refresh();
            }
        }
    }
}


bool Grid::Enable(bool enable)
{
    Refresh();
    return wxScrolledWindow::Enable(enable);
}


size_t Grid::getGridCursor() const
{
    return mainWin_->getCursor();
}


int Grid::getBestColumnSize(size_t col) const
{
    if (dataView_ && col < visibleCols_.size())
    {
        const ColumnType type = visibleCols_[col].type_;

        wxClientDC dc(mainWin_);
        dc.SetFont(mainWin_->GetFont()); //harmonize with MainWin::render()

        int maxSize = 0;

        auto rowRange = rowLabelWin_->getRowsOnClient(mainWin_->GetClientRect()); //returns range [begin, end)
        for (auto row = rowRange.first; row < rowRange.second; ++row)
            maxSize = std::max(maxSize, dataView_->getBestSize(dc, row, type));

        return maxSize;
    }
    return -1;
}


void Grid::setColumnWidth(int width, size_t col, GridEventPolicy columnResizeEventPolicy, bool notifyAsync)
{
    if (col < visibleCols_.size())
    {
        VisibleColumn& vcRs = visibleCols_[col];

        const std::vector<int> stretchedWidths = getColStretchedWidths(mainWin_->GetClientSize().GetWidth());
        if (stretchedWidths.size() != visibleCols_.size())
        {
            assert(false);
            return;
        }
        //CAVEATS:
        //I. fixed-size columns: normalize offset so that resulting width is at least COLUMN_MIN_WIDTH: this is NOT enforced by getColWidths()!
        //II. stretched columns: do not allow user to set offsets so small that they result in negative (non-normalized) widths: this gives an
        //unusual delay when enlarging the column again later
        width = std::max(width, COLUMN_MIN_WIDTH);

        vcRs.offset_ = width - stretchedWidths[col]; //width := stretchedWidth + offset

        //III. resizing any column should normalize *all* other stretched columns' offsets considering current mainWinWidth!
        // test case:
        //1. have columns, both fixed-size and stretched, fit whole window width
        //2. shrink main window width so that horizontal scrollbars are shown despite the streched column
        //3. shrink a fixed-size column so that the scrollbars vanish and columns cover full width again
        //4. now verify that the stretched column is resizing immediately if main window is enlarged again
        for (size_t col2 = 0; col2 < visibleCols_.size(); ++col2)
            if (visibleCols_[col2].stretch_ > 0) //normalize stretched columns only
                visibleCols_[col2].offset_ = std::max(visibleCols_[col2].offset_, COLUMN_MIN_WIDTH - stretchedWidths[col2]);

        if (columnResizeEventPolicy == ALLOW_GRID_EVENT)
        {
            GridColumnResizeEvent sizeEvent(vcRs.offset_, vcRs.type_);
            if (wxEvtHandler* evtHandler = GetEventHandler())
            {
                if (notifyAsync)
                    evtHandler->AddPendingEvent(sizeEvent);
                else
                    evtHandler->ProcessEvent(sizeEvent);
            }
        }
    }
    else
        assert(false);
}


void Grid::autoSizeColumns(GridEventPolicy columnResizeEventPolicy)
{
    if (allowColumnResize_)
    {
        for (size_t col = 0; col < visibleCols_.size(); ++col)
        {
            const int bestWidth = getBestColumnSize(col); //return -1 on error
            if (bestWidth >= 0)
                setColumnWidth(bestWidth, col, columnResizeEventPolicy, true);
        }
        updateWindowSizes();
        Refresh();
    }
}


std::vector<int> Grid::getColStretchedWidths(int clientWidth) const //final width = (normalized) (stretchedWidth + offset)
{
    assert(clientWidth >= 0);
    clientWidth = std::max(clientWidth, 0);
    int stretchTotal = 0;
    for (const VisibleColumn& vc : visibleCols_)
    {
        assert(vc.stretch_ >= 0);
        stretchTotal += vc.stretch_;
    }

    int remainingWidth = clientWidth;

    std::vector<int> output;

    if (stretchTotal <= 0)
        output.resize(visibleCols_.size()); //fill with zeros
    else
    {
        for (const VisibleColumn& vc : visibleCols_)
        {
            const int width = clientWidth * vc.stretch_ / stretchTotal; //rounds down!
            output.push_back(width);
            remainingWidth -= width;
        }

        //distribute *all* of clientWidth: should suffice to enlarge the first few stretched columns; no need to minimize total absolute error of distribution
        if (remainingWidth > 0)
            for (size_t col2 = 0; col2 < visibleCols_.size(); ++col2)
                if (visibleCols_[col2].stretch_ > 0)
                {
                    ++output[col2];
                    if (--remainingWidth == 0)
                        break;
                }
        assert(remainingWidth == 0);
    }
    return output;
}


std::vector<Grid::ColumnWidth> Grid::getColWidths() const
{
    return getColWidths(mainWin_->GetClientSize().GetWidth());
}


std::vector<Grid::ColumnWidth> Grid::getColWidths(int mainWinWidth) const //evaluate stretched columns
{
    const std::vector<int> stretchedWidths = getColStretchedWidths(mainWinWidth);
    assert(stretchedWidths.size() == visibleCols_.size());

    std::vector<ColumnWidth> output;
    for (size_t col2 = 0; col2 < visibleCols_.size(); ++col2)
    {
        const auto& vc = visibleCols_[col2];
        int width = stretchedWidths[col2] + vc.offset_;

        if (vc.stretch_ > 0)
            width = std::max(width, COLUMN_MIN_WIDTH); //normalization really needed here: e.g. smaller main window would result in negative width
        else
            width = std::max(width, 0); //support smaller width than COLUMN_MIN_WIDTH if set via configuration

        output.emplace_back(vc.type_, width);
    }
    return output;
}


int Grid::getColWidthsSum(int mainWinWidth) const
{
    int sum = 0;
    for (const ColumnWidth& cw : getColWidths(mainWinWidth))
        sum += cw.width_;
    return sum;
}
