Created
May 23, 2022 00:56
-
-
Save kjk/5468acb312d63f6ec39d4d56f45f6d3a to your computer and use it in GitHub Desktop.
sumatra tabs (made with https://codeeval.dev)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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