Skip to content

Instantly share code, notes, and snippets.

@kjk
Created May 23, 2022 00:56
Show Gist options
  • Save kjk/5468acb312d63f6ec39d4d56f45f6d3a to your computer and use it in GitHub Desktop.
Save kjk/5468acb312d63f6ec39d4d56f45f6d3a to your computer and use it in GitHub Desktop.
sumatra tabs (made with https://codeeval.dev)
/* Copyright 2022 the SumatraPDF project authors (see AUTHORS file).
License: Simplified BSD (see COPYING.BSD) */
#include "utils/BaseUtil.h"
#include "utils/ScopedWin.h"
#include "utils/Dpi.h"
#include "utils/WinUtil.h"
#include "wingui/Layout.h"
#include "wingui/Window.h"
#include "wingui/TabsCtrl.h"
#include "utils/Log.h"
#define COL_BLACK RGB(0x00, 0x00, 0x00)
#define COL_WHITE RGB(0xff, 0xff, 0xff)
#define COL_RED RGB(0xff, 0x00, 0x00)
#define COL_LIGHT_GRAY RGB(0xde, 0xde, 0xde)
#define COL_LIGHTER_GRAY RGB(0xee, 0xee, 0xee)
#define COL_DARK_GRAY RGB(0x42, 0x42, 0x42)
// desired space between top of the text in tab and top of the tab
#define PADDING_TOP 4
// desired space between bottom of the text in tab and bottom of the tab
#define PADDING_BOTTOM 4
// space to the left of tab label
#define PADDING_LEFT 8
// empty space to the righ of tab label
#define PADDING_RIGHT 8
// TODO: implement a max width for the tab
enum class Tab {
Selected = 0,
Background = 1,
Highlighted = 2,
};
static str::WStr wstrFromUtf8(const str::Str& str) {
auto s = ToWstrTemp(str.LendData(), str.size());
return str::WStr(s);
}
TabItem::TabItem(const char* title, const char* toolTip) {
this->title = title;
this->toolTip = toolTip;
}
class TabItemInfo {
public:
str::WStr title;
str::WStr toolTip;
SIZE titleSize;
// area for this tab item inside the tab window
RECT tabRect;
POINT titlePos;
RECT closeRect;
HWND hwndTooltip;
};
class TabsCtrlPrivate {
public:
explicit TabsCtrlPrivate(HWND hwnd) {
this->hwnd = hwnd;
}
~TabsCtrlPrivate() {
DeleteObject(font);
}
HWND hwnd = nullptr;
HFONT font = nullptr;
// TODO: logFont is not used anymore, keep it for debugging?
LOGFONTW logFont{}; // info that corresponds to font
TEXTMETRIC fontMetrics{};
int fontDy = 0;
SIZE size{}; // current size of the control's window
SIZE idealSize{}; // ideal size as calculated during layout
int tabIdxUnderCursor = -1; // -1 if none under cursor
bool isCursorOverClose = false;
TabsCtrlState* state = nullptr;
// each TabItemInfo orresponds to TabItem from state->tabs, same order
Vec<TabItemInfo*> tabInfos;
};
static long GetIdealDy(TabsCtrl* ctrl) {
auto priv = ctrl->priv;
int padTop = PADDING_TOP;
int padBottom = PADDING_BOTTOM;
DpiScale(priv->hwnd, padTop, padBottom);
return priv->fontDy + padTop + padBottom;
}
static HWND CreateTooltipForRect(HWND parent, const WCHAR* s, RECT& r) {
HMODULE h = GetModuleHandleW(nullptr);
DWORD dwStyleEx = WS_EX_TOPMOST;
DWORD dwStyle = WS_POPUP | TTS_NOPREFIX | TTS_ALWAYSTIP;
HWND hwnd = CreateWindowExW(dwStyleEx, TOOLTIPS_CLASSW, nullptr, dwStyle, CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT, parent, nullptr, h, nullptr);
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
TOOLINFOW ti{};
ti.cbSize = sizeof(TOOLINFO);
ti.uFlags = TTF_SUBCLASS;
ti.hwnd = parent;
ti.hinst = h;
ti.lpszText = (WCHAR*)s;
ti.rect = r;
SendMessageW(hwnd, TTM_ADDTOOLW, 0, (LPARAM)&ti);
SendMessageW(hwnd, TTM_ACTIVATE, TRUE, 0);
return hwnd;
}
void LayoutTabs(TabsCtrl* ctrl) {
auto priv = ctrl->priv;
long x = 0;
long dy = priv->size.cy;
auto idealDy = GetIdealDy(ctrl);
int padLeft = PADDING_LEFT;
int padRight = PADDING_RIGHT;
DpiScale(priv->hwnd, padLeft, padRight);
long closeButtonDy = (priv->fontMetrics.tmAscent / 2) + DpiScale(priv->hwnd, 1);
long closeButtonY = (dy - closeButtonDy) / 2;
if (closeButtonY < 0) {
closeButtonDy = dy - 2;
closeButtonY = 2;
}
for (auto& ti : priv->tabInfos) {
long xStart = x;
x += padLeft;
auto sz = ti->titleSize;
// position y of title text and 'x' circle
long titleY = 0;
if (dy > sz.cy) {
titleY = (dy - sz.cy) / 2;
}
ti->titlePos = MakePoint(x, titleY);
// TODO: implement max dx of the tab
x += sz.cx;
x += padRight;
ti->closeRect = MakeRect(x, closeButtonY, closeButtonDy, closeButtonDy);
x += closeButtonDy;
x += padRight;
long dx = (x - xStart);
ti->tabRect = MakeRect(xStart, 0, dx, dy);
if (!ti->toolTip.IsEmpty()) {
if (ti->hwndTooltip) {
DestroyWindow(ti->hwndTooltip);
}
ti->hwndTooltip = CreateTooltipForRect(priv->hwnd, ti->toolTip.Get(), ti->tabRect);
}
}
priv->idealSize = MakeSize(x, idealDy);
// TODO: if dx > size of the tab, we should shrink the tabs
TriggerRepaint(priv->hwnd);
}
static LRESULT CALLBACK TabsParentProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp, __unused UINT_PTR uIdSubclass,
__unused DWORD_PTR dwRefData) {
// TabsCtrl *w = (TabsCtrl *)dwRefData;
// CrashIf(GetParent(ctrl->hwnd) != (HWND)lp);
return DefSubclassProc(hwnd, msg, wp, lp);
}
static void PaintClose(HWND hwnd, HDC hdc, RECT& r, bool isHighlighted) {
auto x = r.left;
auto y = r.top;
auto dx = RectDx(r);
auto dy = RectDy(r);
COLORREF lineCol = COL_BLACK;
if (isHighlighted) {
int p = 3;
DpiScale(hwnd, p);
AutoDeleteBrush brush(CreateSolidBrush(COL_RED));
RECT r2 = r;
r2.left -= p;
r2.right += p;
r2.top -= p;
r2.bottom += p;
FillRect(hdc, &r2, brush);
lineCol = COL_WHITE;
}
AutoDeletePen pen(CreatePen(PS_SOLID, 2, lineCol));
ScopedSelectPen p(hdc, pen);
MoveToEx(hdc, x, y, nullptr);
LineTo(hdc, x + dx, y + dy);
MoveToEx(hdc, x + dx, y, nullptr);
LineTo(hdc, x, y + dy);
}
static void Paint(TabsCtrl* ctrl) {
auto priv = ctrl->priv;
HWND hwnd = priv->hwnd;
PAINTSTRUCT ps;
RECT rc = GetClientRect(hwnd);
HDC hdc = BeginPaint(hwnd, &ps);
AutoDeleteBrush brush(CreateSolidBrush(COL_LIGHTER_GRAY));
FillRect(hdc, &rc, brush);
ScopedSelectFont f(hdc, priv->font);
uint opts = ETO_OPAQUE;
int padLeft = PADDING_LEFT;
DpiScale(priv->hwnd, padLeft);
int tabIdx = 0;
for (const auto& ti : priv->tabInfos) {
if (ti->title.IsEmpty()) {
continue;
}
auto tabType = Tab::Background;
if (tabIdx == priv->state->selectedItem) {
tabType = Tab::Selected;
} else if (tabIdx == priv->tabIdxUnderCursor) {
tabType = Tab::Highlighted;
}
COLORREF bgCol = COL_LIGHTER_GRAY;
COLORREF txtCol = COL_DARK_GRAY;
bool paintClose = false;
switch (tabType) {
case Tab::Background:
bgCol = COL_LIGHTER_GRAY;
txtCol = COL_DARK_GRAY;
break;
case Tab::Selected:
bgCol = COL_WHITE;
txtCol = COL_DARK_GRAY;
paintClose = true;
break;
case Tab::Highlighted:
bgCol = COL_LIGHT_GRAY;
txtCol = COL_BLACK;
paintClose = true;
break;
}
SetTextColor(hdc, txtCol);
SetBkColor(hdc, bgCol);
auto tabRect = ti->tabRect;
AutoDeleteBrush brush2(CreateSolidBrush(bgCol));
FillRect(hdc, &tabRect, brush2);
auto pos = ti->titlePos;
int x = pos.x;
int y = pos.y;
const WCHAR* s = ti->title.Get();
uint sLen = (uint)ti->title.size();
ExtTextOutW(hdc, x, y, opts, nullptr, s, sLen, nullptr);
if (paintClose) {
bool isCursorOverClose = priv->isCursorOverClose && (tabIdx == priv->tabIdxUnderCursor);
PaintClose(hwnd, hdc, ti->closeRect, isCursorOverClose);
}
tabIdx++;
}
EndPaint(hwnd, &ps);
}
static void SetTabUnderCursor(TabsCtrl* ctrl, int tabUnderCursor, bool isMouseOverClose) {
auto priv = ctrl->priv;
if (priv->tabIdxUnderCursor == tabUnderCursor && priv->isCursorOverClose == isMouseOverClose) {
return;
}
priv->tabIdxUnderCursor = tabUnderCursor;
priv->isCursorOverClose = isMouseOverClose;
TriggerRepaint(priv->hwnd);
}
static int TabFromMousePos(TabsCtrl* ctrl, int x, int y, bool& isMouseOverClose) {
POINT mousePos = {x, y};
for (size_t i = 0; i < ctrl->priv->tabInfos.size(); i++) {
auto& ti = ctrl->priv->tabInfos[i];
if (PtInRect(&ti->tabRect, mousePos)) {
isMouseOverClose = PtInRect(&ti->closeRect, mousePos);
return (int)i;
}
}
return -1;
}
static void OnMouseMove(TabsCtrl* ctrl) {
auto priv = ctrl->priv;
auto mousePos = GetCursorPosInHwnd(priv->hwnd);
bool isMouseOverClose = false;
auto tabIdx = TabFromMousePos(ctrl, mousePos.x, mousePos.y, isMouseOverClose);
SetTabUnderCursor(ctrl, tabIdx, isMouseOverClose);
TrackMouseLeave(priv->hwnd);
}
static void OnLeftButtonUp(TabsCtrl* ctrl) {
auto priv = ctrl->priv;
auto mousePos = GetCursorPosInHwnd(priv->hwnd);
bool isMouseOverClose;
auto tabIdx = TabFromMousePos(ctrl, mousePos.x, mousePos.y, isMouseOverClose);
if (tabIdx == -1) {
return;
}
if (isMouseOverClose) {
if (ctrl->onTabClosed) {
ctrl->onTabClosed(ctrl, priv->state, tabIdx);
}
return;
}
if (tabIdx == priv->state->selectedItem) {
return;
}
if (ctrl->onTabSelected) {
ctrl->onTabSelected(ctrl, priv->state, tabIdx);
}
}
static LRESULT CALLBACK TabsProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp, __unused UINT_PTR uIdSubclass,
DWORD_PTR dwRefData) {
TabsCtrl* ctrl = (TabsCtrl*)dwRefData;
TabsCtrlPrivate* priv = ctrl->priv;
// CrashIf(ctrl->hwnd != (HWND)lp);
// TraceMsg(msg);
if (WM_ERASEBKGND == msg) {
return TRUE; // tells Windows we handle background erasing so it doesn't do it
}
// This is needed in order to receive WM_MOUSEMOVE messages
if (WM_NCHITTEST == msg) {
// TODO: or just return HTCLIENT always?
if (hwnd == GetCapture()) {
return HTCLIENT;
}
auto mousePos = GetCursorPosInHwnd(ctrl->priv->hwnd);
bool isMouseOverClose;
auto tabIdx = TabFromMousePos(ctrl, mousePos.x, mousePos.y, isMouseOverClose);
if (-1 == tabIdx) {
return HTTRANSPARENT;
}
return HTCLIENT;
}
if (WM_NCDESTROY == msg) {
RemoveWindowSubclass(GetParent(priv->hwnd), TabsParentProc, 0);
RemoveWindowSubclass(priv->hwnd, TabsProc, 0);
return DefSubclassProc(hwnd, msg, wp, lp);
}
if (WM_PAINT == msg) {
CrashIf(priv->hwnd != hwnd);
Paint(ctrl);
return 0;
}
if (WM_LBUTTONDOWN == msg) {
return 0;
}
if (WM_LBUTTONUP == msg) {
OnLeftButtonUp(ctrl);
return 0;
}
if (WM_MOUSELEAVE == msg) {
SetTabUnderCursor(ctrl, -1, false);
return 0;
}
if (WM_MOUSEMOVE == msg) {
OnMouseMove(ctrl);
return 0;
}
if (WM_SIZE == msg) {
long dx = LOWORD(lp);
long dy = HIWORD(lp);
priv->size = MakeSize(dx, dy);
LayoutTabs(ctrl);
return 0;
}
return DefSubclassProc(hwnd, msg, wp, lp);
}
TabsCtrl* AllocTabsCtrl(HWND parent, RECT initialPosition) {
auto w = new TabsCtrl;
w->parent = parent;
w->initialPos = initialPosition;
return w;
}
void SetFont(TabsCtrl* ctrl, HFONT font) {
auto priv = ctrl->priv;
priv->font = font;
GetObject(font, sizeof(LOGFONTW), &priv->logFont);
ScopedGetDC hdc(priv->hwnd);
ScopedSelectFont prevFont(hdc, priv->font);
GetTextMetrics(hdc, &priv->fontMetrics);
priv->fontDy = priv->fontMetrics.tmHeight;
}
bool CreateTabsCtrl(TabsCtrl* ctrl) {
auto r = ctrl->initialPos;
auto x = r.left;
auto y = r.top;
auto dx = RectDx(r);
auto dy = RectDy(r);
DWORD exStyle = 0;
DWORD style = WS_CHILD | WS_CLIPSIBLINGS | WS_VISIBLE | TCS_FOCUSNEVER | TCS_FIXEDWIDTH | TCS_FORCELABELLEFT;
HINSTANCE h = GetModuleHandleW(nullptr);
auto hwnd = CreateWindowExW(exStyle, WC_TABCONTROL, L"", style, x, y, dx, dy, ctrl->parent, nullptr, h, ctrl);
if (hwnd == nullptr) {
return false;
}
auto priv = new TabsCtrlPrivate(hwnd);
r = ctrl->initialPos;
priv->size = MakeSize(RectDx(r), RectDy(r));
priv->hwnd = hwnd;
ctrl->priv = priv;
SetFont(ctrl, GetDefaultGuiFont());
SetWindowSubclass(hwnd, TabsProc, 0, (DWORD_PTR)ctrl);
// SetWindowSubclass(GetParent(hwnd), TabsParentProc, 0, (DWORD_PTR)ctrl);
return true;
}
void DeleteTabsCtrl(TabsCtrl* ctrl) {
if (ctrl) {
DeleteObject(ctrl->priv->font);
delete ctrl->priv;
}
delete ctrl;
}
void SetState(TabsCtrl* ctrl, TabsCtrlState* state) {
auto priv = ctrl->priv;
priv->state = state;
priv->tabInfos.Reset();
// measure size of tab's title
auto& tabInfos = priv->tabInfos;
for (auto& tab : state->tabs) {
auto ti = new TabItemInfo();
tabInfos.Append(ti);
ti->titleSize = MakeSize(0, 0);
if (!tab->title.IsEmpty()) {
ti->title = wstrFromUtf8(tab->title);
const WCHAR* s = ti->title.Get();
ti->titleSize = TextSizeInHwnd2(priv->hwnd, s, priv->font);
}
if (!tab->toolTip.IsEmpty()) {
ti->toolTip = wstrFromUtf8(tab->toolTip);
}
}
LayoutTabs(ctrl);
// TODO: should use mouse position to determine this
// TODO: calculate isHighlighted
priv->isCursorOverClose = false;
}
SIZE GetIdealSize(TabsCtrl* ctrl) {
return ctrl->priv->idealSize;
}
void SetPos(TabsCtrl* ctrl, RECT& r) {
MoveWindow(ctrl->priv->hwnd, &r);
}
/* ----- */
Kind kindTabs = "tabs";
TabsCtrl2::TabsCtrl2() {
dwStyle = WS_CHILD | WS_CLIPSIBLINGS | TCS_FOCUSNEVER | TCS_FIXEDWIDTH | TCS_FORCELABELLEFT | WS_VISIBLE;
winClass = WC_TABCONTROLW;
kind = kindTabs;
}
TabsCtrl2::~TabsCtrl2() = default;
static void Handle_WM_NOTIFY(void* user, WndEvent* ev) {
CrashIf(ev->msg != WM_NOTIFY);
TabsCtrl2* w = (TabsCtrl2*)user;
ev->w = w; // TODO: is this needed?
CrashIf(GetParent(w->hwnd) != (HWND)ev->hwnd);
LPNMHDR hdr = (LPNMHDR)ev->lp;
if (hdr->code == TTN_GETDISPINFOA) {
logf("Handle_WM_NOTIFY TTN_GETDISPINFOA\n");
} else if (hdr->code == TTN_GETDISPINFOW) {
logf("Handle_WM_NOTIFY TTN_GETDISPINFOW\n");
}
}
bool TabsCtrl2::Create(HWND parent) {
if (createToolTipsHwnd) {
dwStyle |= TCS_TOOLTIPS;
}
bool ok = WindowBase::Create(parent);
if (!ok) {
return false;
}
void* user = this;
RegisterHandlerForMessage(hwnd, WM_NOTIFY, Handle_WM_NOTIFY, user);
if (createToolTipsHwnd) {
HWND ttHwnd = GetToolTipsHwnd();
TOOLINFO ti{0};
ti.cbSize = sizeof(ti);
ti.hwnd = hwnd;
ti.uId = 0;
ti.uFlags = TTF_SUBCLASS;
ti.lpszText = (WCHAR*)L"placeholder tooltip";
SetRectEmpty(&ti.rect);
RECT r = ti.rect;
SendMessage(ttHwnd, TTM_ADDTOOL, 0, (LPARAM)&ti);
}
return true;
}
void TabsCtrl2::WndProc(WndEvent* ev) {
HWND hwnd = ev->hwnd;
#if 0
UINT msg = ev->msg;
WPARAM wp = ev->wp;
LPARAM lp = ev->lp;
DbgLogMsg("tree:", hwnd, msg, wp, ev->lp);
#endif
TabsCtrl2* w = this;
CrashIf(w->hwnd != (HWND)hwnd);
}
Size TabsCtrl2::GetIdealSize() {
Size sz{32, 128};
return sz;
}
int TabsCtrl2::GetTabCount() {
int n = TabCtrl_GetItemCount(hwnd);
return n;
}
int TabsCtrl2::InsertTab(int idx, const char* s) {
CrashIf(idx < 0);
TCITEMW item{0};
item.mask = TCIF_TEXT;
item.pszText = ToWstrTemp(s);
int insertedIdx = TabCtrl_InsertItem(hwnd, idx, &item);
tooltips.InsertAt(idx, "");
return insertedIdx;
}
void TabsCtrl2::RemoveTab(int idx) {
CrashIf(idx < 0);
CrashIf(idx >= GetTabCount());
BOOL ok = TabCtrl_DeleteItem(hwnd, idx);
CrashIf(!ok);
tooltips.RemoveAt(idx);
}
void TabsCtrl2::RemoveAllTabs() {
TabCtrl_DeleteAllItems(hwnd);
tooltips.Reset();
}
void TabsCtrl2::SetTabText(int idx, const char* s) {
CrashIf(idx < 0);
CrashIf(idx >= GetTabCount());
TCITEMW item{0};
item.mask = TCIF_TEXT;
item.pszText = ToWstrTemp(s);
TabCtrl_SetItem(hwnd, idx, &item);
}
// result is valid until next call to GetTabText()
char* TabsCtrl2::GetTabText(int idx) {
CrashIf(idx < 0);
CrashIf(idx >= GetTabCount());
WCHAR buf[512]{};
TCITEMW item{0};
item.mask = TCIF_TEXT;
item.pszText = buf;
item.cchTextMax = dimof(buf) - 1; // -1 just in case
TabCtrl_GetItem(hwnd, idx, &item);
char* s = ToUtf8Temp(buf);
lastTabText.Set(s);
return lastTabText.Get();
}
int TabsCtrl2::GetSelectedTabIndex() {
int idx = TabCtrl_GetCurSel(hwnd);
return idx;
}
int TabsCtrl2::SetSelectedTabByIndex(int idx) {
int prevSelectedIdx = TabCtrl_SetCurSel(hwnd, idx);
return prevSelectedIdx;
}
void TabsCtrl2::SetItemSize(Size sz) {
TabCtrl_SetItemSize(hwnd, sz.dx, sz.dy);
}
void TabsCtrl2::SetToolTipsHwnd(HWND hwndTooltip) {
TabCtrl_SetToolTips(hwnd, hwndTooltip);
}
HWND TabsCtrl2::GetToolTipsHwnd() {
HWND res = TabCtrl_GetToolTips(hwnd);
return res;
}
// TODO: this is a nasty implementation
// should probably TTM_ADDTOOL for each tab item
// we could re-calculate it in SetItemSize()
void TabsCtrl2::MaybeUpdateTooltip() {
// logf("MaybeUpdateTooltip() start\n");
HWND ttHwnd = GetToolTipsHwnd();
if (!ttHwnd) {
return;
}
{
TOOLINFO ti{0};
ti.cbSize = sizeof(ti);
ti.hwnd = hwnd;
ti.uId = 0;
SendMessage(ttHwnd, TTM_DELTOOL, 0, (LPARAM)&ti);
}
{
TOOLINFO ti{0};
ti.cbSize = sizeof(ti);
ti.hwnd = hwnd;
ti.uFlags = TTF_SUBCLASS;
// ti.lpszText = LPSTR_TEXTCALLBACK;
WCHAR* ws = ToWstrTemp(currTooltipText.Get());
ti.lpszText = ws;
ti.uId = 0;
GetClientRect(hwnd, &ti.rect);
SendMessage(ttHwnd, TTM_ADDTOOL, 0, (LPARAM)&ti);
}
}
void TabsCtrl2::MaybeUpdateTooltipText(int idx) {
HWND ttHwnd = GetToolTipsHwnd();
if (!ttHwnd) {
return;
}
const char* tooltip = GetTooltip(idx);
if (!tooltip) {
// TODO: remove tooltip
return;
}
currTooltipText.Set(tooltip);
#if 1
MaybeUpdateTooltip();
#else
// TODO: why this doesn't work?
TOOLINFO ti{0};
ti.cbSize = sizeof(ti);
ti.hwnd = hwnd;
ti.uFlags = TTF_SUBCLASS;
ti.lpszText = currTooltipText.Get();
ti.uId = 0;
GetClientRect(hwnd, &ti.rect);
SendMessage(ttHwnd, TTM_UPDATETIPTEXT, 0, (LPARAM)&ti);
#endif
// SendMessage(ttHwnd, TTM_UPDATE, 0, 0);
SendMessage(ttHwnd, TTM_POP, 0, 0);
SendMessage(ttHwnd, TTM_POPUP, 0, 0);
// logf(L"MaybeUpdateTooltipText: %s\n", tooltip);
}
void TabsCtrl2::SetTooltip(int idx, const char* s) {
tooltips.SetAt(idx, s);
}
const char* TabsCtrl2::GetTooltip(int idx) {
if (idx >= tooltips.Size()) {
return nullptr;
}
char* res = tooltips.at(idx);
return res;
}
struct TabsCtrl;
class TabsCtrlState;
using TabSelectedCb = std::function<void(TabsCtrl*, TabsCtrlState*, int)>;
using TabClosedCb = std::function<void(TabsCtrl*, TabsCtrlState*, int)>;
class TabItem {
public:
TabItem(const char* title, const char* toolTip);
str::Str title;
str::Str toolTip;
};
class TabsCtrlState {
public:
Vec<TabItem*> tabs;
int selectedItem = 0;
};
class TabsCtrlPrivate;
struct TabsCtrl {
// creation parameters. must be set before CreateTabsCtrl
HWND parent = nullptr;
RECT initialPos{};
TabSelectedCb onTabSelected = nullptr;
TabClosedCb onTabClosed = nullptr;
TabsCtrlPrivate* priv;
};
/* Creation sequence:
- AllocTabsCtrl()
- set creation parameters
- CreateTabsCtrl()
*/
TabsCtrl* AllocTabsCtrl(HWND parent, RECT initialPosition);
bool CreateTabsCtrl(TabsCtrl*);
void DeleteTabsCtrl(TabsCtrl*);
void SetState(TabsCtrl*, TabsCtrlState*);
SIZE GetIdealSize(TabsCtrl*);
void SetPos(TabsCtrl*, RECT&);
void SetFont(TabsCtrl*, HFONT);
#include "utils/BaseUtil.h"
#include "utils/ScopedWin.h"
#include "utils/WinUtil.h"
#include "test-app.h"
#include "wingui/Layout.h"
#include "wingui/Window.h"
#include "wingui/TabsCtrl.h"
static HINSTANCE hInst;
static const WCHAR* gWindowTitle = L"Test application";
static const WCHAR* WIN_CLASS = L"TabTestWndCls";
static TabsCtrl* g_tabsCtrl = nullptr;
static HWND g_hwnd = nullptr;
#define COL_GRAY RGB(0xdd, 0xdd, 0xdd)
#define COL_WHITE RGB(0xff, 0xff, 0xff)
#define COL_BLACK RGB(0, 0, 0)
static void UpdateTabsSize() {
SIZE sz = GetIdealSize(g_tabsCtrl);
RECT tabsPos = MakeRect(10, 10, sz.cx, sz.cy);
SetPos(g_tabsCtrl, tabsPos);
TriggerRepaint(g_hwnd);
}
static void Draw(HWND hwnd, HDC hdc) {
RECT rc = GetClientRect(hwnd);
AutoDeleteBrush brush(CreateSolidBrush(COL_GRAY));
FillRect(hdc, &rc, brush);
}
static void OnTabSelected(TabsCtrl* tabsCtrl, TabsCtrlState* currState, int selectedTabIdx) {
CrashIf(g_tabsCtrl != tabsCtrl);
CrashIf(currState->selectedItem == selectedTabIdx);
currState->selectedItem = selectedTabIdx;
SetState(tabsCtrl, currState);
}
static void OnTabClosed(TabsCtrl* tabsCtrl, TabsCtrlState* currState, int tabIdx) {
CrashIf(g_tabsCtrl != tabsCtrl);
currState->tabs.RemoveAt(tabIdx);
if (currState->selectedItem == tabIdx) {
if (currState->selectedItem > 0) {
currState->selectedItem--;
}
}
SetState(tabsCtrl, currState);
UpdateTabsSize();
}
static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
switch (msg) {
case WM_CREATE:
break;
case WM_COMMAND: {
int wmId = LOWORD(wp);
switch (wmId) {
case IDM_EXIT:
DestroyWindow(hwnd);
break;
default:
return DefWindowProc(hwnd, msg, wp, lp);
}
} break;
case WM_PAINT: {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
Draw(hwnd, hdc);
EndPaint(hwnd, &ps);
// ValidateRect(hwnd, NULL);
} break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hwnd, msg, wp, lp);
}
return 0;
}
static ATOM RegisterWinClass(HINSTANCE hInstance) {
WNDCLASSEXW wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_TESTWIN));
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_TESTWIN);
wcex.lpszClassName = WIN_CLASS;
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
return RegisterClassExW(&wcex);
}
static BOOL InitInstance(HINSTANCE hInstance, int nCmdShow) {
hInst = hInstance;
const WCHAR* cls = WIN_CLASS;
DWORD dwExStyle = 0;
DWORD dwStyle = WS_OVERLAPPEDWINDOW;
int dx = 640;
int dy = 480;
HWND hwnd = CreateWindowExW(dwExStyle, cls, gWindowTitle, dwStyle, CW_USEDEFAULT, CW_USEDEFAULT, dx, dy, nullptr,
nullptr, hInstance, nullptr);
if (!hwnd)
return FALSE;
g_hwnd = hwnd;
int fontDy = GetSizeOfDefaultGuiFont();
RECT tabsPos = MakeRect(8, 8, 320, fontDy + 8);
g_tabsCtrl = AllocTabsCtrl(hwnd, tabsPos);
g_tabsCtrl->onTabSelected = OnTabSelected;
g_tabsCtrl->onTabClosed = OnTabClosed;
auto ok = CreateTabsCtrl(g_tabsCtrl);
CrashIf(!ok);
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
return TRUE;
}
int TestTab(HINSTANCE hInstance, int nCmdShow) {
RegisterWinClass(hInstance);
if (!InitInstance(hInstance, nCmdShow)) {
CrashAlwaysIf(true);
return FALSE;
}
auto tabsState = new TabsCtrlState();
tabsState->selectedItem = 0;
std::array<TabItem*, 3> tabs = {
new TabItem{"tab1", "tab 1 tooltip"},
new TabItem{"tab 2 with a very long name", ""},
new TabItem{"another tab", "another tab tooltip"},
};
for (auto& tab : tabs) {
tabsState->tabs.Append(tab);
}
SetState(g_tabsCtrl, tabsState);
UpdateTabsSize();
HACCEL accelTable = LoadAccelerators(hInst, MAKEINTRESOURCE(IDC_TESTWIN));
auto res = RunMessageLoop(accelTable, nullptr);
return res;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment