diff options
Diffstat (limited to 'src/libui_sdl/libui/windows')
63 files changed, 11850 insertions, 0 deletions
diff --git a/src/libui_sdl/libui/windows/CMakeLists.txt b/src/libui_sdl/libui/windows/CMakeLists.txt new file mode 100644 index 0000000..4695eb4 --- /dev/null +++ b/src/libui_sdl/libui/windows/CMakeLists.txt @@ -0,0 +1,91 @@ +# 3 june 2016 + +list(APPEND _LIBUI_SOURCES + windows/alloc.cpp + windows/area.cpp + windows/areadraw.cpp + windows/areaevents.cpp + windows/areascroll.cpp + windows/areautil.cpp + windows/box.cpp + windows/button.cpp + windows/checkbox.cpp + windows/colorbutton.cpp + windows/colordialog.cpp + windows/combobox.cpp + windows/container.cpp + windows/control.cpp + windows/d2dscratch.cpp + windows/datetimepicker.cpp + windows/debug.cpp + windows/draw.cpp + windows/drawmatrix.cpp + windows/drawpath.cpp + windows/drawtext.cpp + windows/dwrite.cpp + windows/editablecombo.cpp + windows/entry.cpp + windows/events.cpp + windows/fontbutton.cpp + windows/fontdialog.cpp + windows/form.cpp + windows/graphemes.cpp + windows/grid.cpp + windows/group.cpp + windows/init.cpp + windows/label.cpp + windows/main.cpp + windows/menu.cpp + windows/multilineentry.cpp + windows/parent.cpp + windows/progressbar.cpp + windows/radiobuttons.cpp + windows/separator.cpp + windows/sizing.cpp + windows/slider.cpp + windows/spinbox.cpp + windows/stddialogs.cpp + windows/tab.cpp + windows/tabpage.cpp + windows/text.cpp + windows/utf16.cpp + windows/utilwin.cpp + windows/window.cpp + windows/winpublic.cpp + windows/winutil.cpp + windows/resources.rc +) +set(_LIBUI_SOURCES ${_LIBUI_SOURCES} PARENT_SCOPE) + +list(APPEND _LIBUI_INCLUDEDIRS + windows +) +set(_LIBUI_INCLUDEDIRS _LIBUI_INCLUDEDIRS PARENT_SCOPE) + +# Windows won't link resources in static libraries; we need to provide the libui.res file in this case. +set(_LIBUINAME libui PARENT_SCOPE) +if(NOT BUILD_SHARED_LIBS) + set(_LIBUI_STATIC_RES ${CMAKE_ARCHIVE_OUTPUT_DIRECTORY}/libui.res PARENT_SCOPE) +endif() +macro(_handle_static) + # TODO this full path feels hacky + add_custom_command( + TARGET libui POST_BUILD + COMMAND + ${CMAKE_COMMAND} -E copy $<TARGET_PROPERTY:libui,BINARY_DIR>/CMakeFiles/libui.dir/windows/resources.rc.* ${_LIBUI_STATIC_RES} + COMMENT "Copying libui.res") +endmacro() + +# notice that usp10 comes before gdi32 +# TODO prune this list +set(_LIBUI_LIBS + user32 kernel32 usp10 gdi32 comctl32 uxtheme msimg32 comdlg32 d2d1 dwrite ole32 oleaut32 oleacc uuid +PARENT_SCOPE) + +if(NOT MSVC) + if(BUILD_SHARED_LIBS) + message(FATAL_ERROR + "Sorry, but libui for Windows can currently only be built as a static library with MinGW. You will need to either build as a static library or switch to MSVC." + ) + endif() +endif() diff --git a/src/libui_sdl/libui/windows/_uipriv_migrate.hpp b/src/libui_sdl/libui/windows/_uipriv_migrate.hpp new file mode 100644 index 0000000..13d3670 --- /dev/null +++ b/src/libui_sdl/libui/windows/_uipriv_migrate.hpp @@ -0,0 +1,61 @@ + +// menu.c +extern HMENU makeMenubar(void); +extern const uiMenuItem *menuIDToItem(UINT_PTR); +extern void runMenuEvent(WORD, uiWindow *); +extern void freeMenubar(HMENU); +extern void uninitMenus(void); + +// draw.c +extern HRESULT initDraw(void); +extern void uninitDraw(void); +extern ID2D1HwndRenderTarget *makeHWNDRenderTarget(HWND hwnd); +extern uiDrawContext *newContext(ID2D1RenderTarget *); +extern void freeContext(uiDrawContext *); + +// dwrite.cpp +#ifdef __cplusplus +extern IDWriteFactory *dwfactory; +#endif +extern HRESULT initDrawText(void); +extern void uninitDrawText(void); +#ifdef __cplusplus +struct fontCollection { + IDWriteFontCollection *fonts; + WCHAR userLocale[LOCALE_NAME_MAX_LENGTH]; + int userLocaleSuccess; +}; +extern fontCollection *loadFontCollection(void); +extern WCHAR *fontCollectionFamilyName(fontCollection *fc, IDWriteFontFamily *family); +extern void fontCollectionFree(fontCollection *fc); +extern WCHAR *fontCollectionCorrectString(fontCollection *fc, IDWriteLocalizedStrings *names); +#endif + +// drawtext.cpp +#ifdef __cplusplus +extern uiDrawTextFont *mkTextFont(IDWriteFont *df, BOOL addRef, WCHAR *family, BOOL copyFamily, double size); +struct dwriteAttr { + uiDrawTextWeight weight; + uiDrawTextItalic italic; + uiDrawTextStretch stretch; + DWRITE_FONT_WEIGHT dweight; + DWRITE_FONT_STYLE ditalic; + DWRITE_FONT_STRETCH dstretch; +}; +extern void attrToDWriteAttr(struct dwriteAttr *attr); +extern void dwriteAttrToAttr(struct dwriteAttr *attr); +#endif + +// fontdialog.cpp +#ifdef __cplusplus +struct fontDialogParams { + IDWriteFont *font; + double size; + WCHAR *familyName; + WCHAR *styleName; +}; +extern BOOL showFontDialog(HWND parent, struct fontDialogParams *params); +extern void loadInitialFontDialogParams(struct fontDialogParams *params); +extern void destroyFontDialogParams(struct fontDialogParams *params); +extern WCHAR *fontDialogParamsToString(struct fontDialogParams *params); +#endif diff --git a/src/libui_sdl/libui/windows/alloc.cpp b/src/libui_sdl/libui/windows/alloc.cpp new file mode 100644 index 0000000..eeee3ad --- /dev/null +++ b/src/libui_sdl/libui/windows/alloc.cpp @@ -0,0 +1,63 @@ +// 4 december 2014 +#include "uipriv_windows.hpp" + +typedef std::vector<uint8_t> byteArray; + +static std::map<uint8_t *, byteArray *> heap; +static std::map<byteArray *, const char *> types; + +void initAlloc(void) +{ + // do nothing +} + +void uninitAlloc(void) +{ + std::ostringstream oss; + std::string ossstr; // keep alive, just to be safe + + if (heap.size() == 0) + return; + for (const auto &alloc : heap) + // note the void * cast; otherwise it'll be treated as a string + oss << (void *) (alloc.first) << " " << types[alloc.second] << "\n"; + ossstr = oss.str(); + userbug("Some data was leaked; either you left a uiControl lying around or there's a bug in libui itself. Leaked data:\n%s", ossstr.c_str()); +} + +#define rawBytes(pa) (&((*pa)[0])) + +void *uiAlloc(size_t size, const char *type) +{ + byteArray *out; + + out = new byteArray(size, 0); + heap[rawBytes(out)] = out; + types[out] = type; + return rawBytes(out); +} + +void *uiRealloc(void *_p, size_t size, const char *type) +{ + uint8_t *p = (uint8_t *) _p; + byteArray *arr; + + if (p == NULL) + return uiAlloc(size, type); + arr = heap[p]; + arr->resize(size, 0); + heap.erase(p); + heap[rawBytes(arr)] = arr; + return rawBytes(arr); +} + +void uiFree(void *_p) +{ + uint8_t *p = (uint8_t *) _p; + + if (p == NULL) + implbug("attempt to uiFree(NULL)"); + types.erase(heap[p]); + delete heap[p]; + heap.erase(p); +} diff --git a/src/libui_sdl/libui/windows/area.cpp b/src/libui_sdl/libui/windows/area.cpp new file mode 100644 index 0000000..ab69ff1 --- /dev/null +++ b/src/libui_sdl/libui/windows/area.cpp @@ -0,0 +1,206 @@ +// 8 september 2015 +#include "uipriv_windows.hpp" +#include "area.hpp" + +// TODO handle WM_DESTROY/WM_NCDESTROY +// TODO same for other Direct2D stuff +static LRESULT CALLBACK areaWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + uiArea *a; + CREATESTRUCTW *cs = (CREATESTRUCTW *) lParam; + RECT client; + WINDOWPOS *wp = (WINDOWPOS *) lParam; + LRESULT lResult; + + a = (uiArea *) GetWindowLongPtrW(hwnd, GWLP_USERDATA); + if (a == NULL) { + if (uMsg == WM_CREATE) { + a = (uiArea *) (cs->lpCreateParams); + // assign a->hwnd here so we can use it immediately + a->hwnd = hwnd; + SetWindowLongPtrW(hwnd, GWLP_USERDATA, (LONG_PTR) a); + } + // fall through to DefWindowProcW() anyway + return DefWindowProcW(hwnd, uMsg, wParam, lParam); + } + + // always recreate the render target if necessary + if (a->rt == NULL) + a->rt = makeHWNDRenderTarget(a->hwnd); + + if (areaDoDraw(a, uMsg, wParam, lParam, &lResult) != FALSE) + return lResult; + + if (uMsg == WM_WINDOWPOSCHANGED) { + if ((wp->flags & SWP_NOSIZE) != 0) + return DefWindowProcW(hwnd, uMsg, wParam, lParam); + uiWindowsEnsureGetClientRect(a->hwnd, &client); + areaDrawOnResize(a, &client); + areaScrollOnResize(a, &client); + return 0; + } + + if (areaDoScroll(a, uMsg, wParam, lParam, &lResult) != FALSE) + return lResult; + if (areaDoEvents(a, uMsg, wParam, lParam, &lResult) != FALSE) + return lResult; + + // nothing done + return DefWindowProc(hwnd, uMsg, wParam, lParam); +} + +// control implementation + +uiWindowsControlAllDefaults(uiArea) + +static void uiAreaMinimumSize(uiWindowsControl *c, int *width, int *height) +{ + // TODO + *width = 0; + *height = 0; +} + +ATOM registerAreaClass(HICON hDefaultIcon, HCURSOR hDefaultCursor) +{ + WNDCLASSW wc; + + ZeroMemory(&wc, sizeof (WNDCLASSW)); + wc.lpszClassName = areaClass; + wc.lpfnWndProc = areaWndProc; + wc.hInstance = hInstance; + wc.hIcon = hDefaultIcon; + wc.hCursor = hDefaultCursor; + wc.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1); + // this is just to be safe; see the InvalidateRect() call in the WM_WINDOWPOSCHANGED handler for more details + wc.style = CS_HREDRAW | CS_VREDRAW; + return RegisterClassW(&wc); +} + +void unregisterArea(void) +{ + if (UnregisterClassW(areaClass, hInstance) == 0) + logLastError(L"error unregistering uiArea window class"); +} + +void uiAreaSetSize(uiArea *a, int width, int height) +{ + a->scrollWidth = width; + a->scrollHeight = height; + areaUpdateScroll(a); +} + +void uiAreaQueueRedrawAll(uiArea *a) +{ + // don't erase the background; we do that ourselves in doPaint() + invalidateRect(a->hwnd, NULL, FALSE); +} + +void uiAreaScrollTo(uiArea *a, double x, double y, double width, double height) +{ + // TODO +} + +void uiAreaBeginUserWindowMove(uiArea *a) +{ + HWND toplevel; + + // TODO restrict execution + ReleaseCapture(); // TODO use properly and reset internal data structures + toplevel = parentToplevel(a->hwnd); + if (toplevel == NULL) { + // TODO + return; + } + // see http://stackoverflow.com/questions/40249940/how-do-i-initiate-a-user-mouse-driven-move-or-resize-for-custom-window-borders-o#40250654 + SendMessageW(toplevel, WM_SYSCOMMAND, + SC_MOVE | 2, 0); +} + +void uiAreaBeginUserWindowResize(uiArea *a, uiWindowResizeEdge edge) +{ + HWND toplevel; + WPARAM wParam; + + // TODO restrict execution + ReleaseCapture(); // TODO use properly and reset internal data structures + toplevel = parentToplevel(a->hwnd); + if (toplevel == NULL) { + // TODO + return; + } + // see http://stackoverflow.com/questions/40249940/how-do-i-initiate-a-user-mouse-driven-move-or-resize-for-custom-window-borders-o#40250654 + wParam = SC_SIZE; + switch (edge) { + case uiWindowResizeEdgeLeft: + wParam |= 1; + break; + case uiWindowResizeEdgeTop: + wParam |= 3; + break; + case uiWindowResizeEdgeRight: + wParam |= 2; + break; + case uiWindowResizeEdgeBottom: + wParam |= 6; + break; + case uiWindowResizeEdgeTopLeft: + wParam |= 4; + break; + case uiWindowResizeEdgeTopRight: + wParam |= 5; + break; + case uiWindowResizeEdgeBottomLeft: + wParam |= 7; + break; + case uiWindowResizeEdgeBottomRight: + wParam |= 8; + break; + } + SendMessageW(toplevel, WM_SYSCOMMAND, + wParam, 0); +} + +uiArea *uiNewArea(uiAreaHandler *ah) +{ + uiArea *a; + + uiWindowsNewControl(uiArea, a); + + a->ah = ah; + a->scrolling = FALSE; + clickCounterReset(&(a->cc)); + + // a->hwnd is assigned in areaWndProc() + uiWindowsEnsureCreateControlHWND(0, + areaClass, L"", + 0, + hInstance, a, + FALSE); + + return a; +} + +uiArea *uiNewScrollingArea(uiAreaHandler *ah, int width, int height) +{ + uiArea *a; + + uiWindowsNewControl(uiArea, a); + + a->ah = ah; + a->scrolling = TRUE; + a->scrollWidth = width; + a->scrollHeight = height; + clickCounterReset(&(a->cc)); + + // a->hwnd is assigned in areaWndProc() + uiWindowsEnsureCreateControlHWND(0, + areaClass, L"", + WS_HSCROLL | WS_VSCROLL, + hInstance, a, + FALSE); + + // set initial scrolling parameters + areaUpdateScroll(a); + + return a; +} diff --git a/src/libui_sdl/libui/windows/area.hpp b/src/libui_sdl/libui/windows/area.hpp new file mode 100644 index 0000000..86a62de --- /dev/null +++ b/src/libui_sdl/libui/windows/area.hpp @@ -0,0 +1,45 @@ +// 18 december 2015 + +// TODOs +// - things look very wrong on initial draw +// - initial scrolling is not set properly +// - should background be inherited from parent control? + +struct uiArea { + uiWindowsControl c; + HWND hwnd; + uiAreaHandler *ah; + + BOOL scrolling; + int scrollWidth; + int scrollHeight; + int hscrollpos; + int vscrollpos; + int hwheelCarry; + int vwheelCarry; + + clickCounter cc; + BOOL capturing; + + BOOL inside; + BOOL tracking; + + ID2D1HwndRenderTarget *rt; +}; + +// areadraw.cpp +extern BOOL areaDoDraw(uiArea *a, UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT *lResult); +extern void areaDrawOnResize(uiArea *, RECT *); + +// areascroll.cpp +extern BOOL areaDoScroll(uiArea *a, UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT *lResult); +extern void areaScrollOnResize(uiArea *, RECT *); +extern void areaUpdateScroll(uiArea *a); + +// areaevents.cpp +extern BOOL areaDoEvents(uiArea *a, UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT *lResult); + +// areautil.cpp +extern void loadAreaSize(uiArea *a, ID2D1RenderTarget *rt, double *width, double *height); +extern void pixelsToDIP(uiArea *a, double *x, double *y); +extern void dipToPixels(uiArea *a, double *x, double *y); diff --git a/src/libui_sdl/libui/windows/areadraw.cpp b/src/libui_sdl/libui/windows/areadraw.cpp new file mode 100644 index 0000000..7b3dc69 --- /dev/null +++ b/src/libui_sdl/libui/windows/areadraw.cpp @@ -0,0 +1,137 @@ +// 8 september 2015 +#include "uipriv_windows.hpp" +#include "area.hpp" + +static HRESULT doPaint(uiArea *a, ID2D1RenderTarget *rt, RECT *clip) +{ + uiAreaHandler *ah = a->ah; + uiAreaDrawParams dp; + COLORREF bgcolorref; + D2D1_COLOR_F bgcolor; + D2D1_MATRIX_3X2_F scrollTransform; + + // no need to save or restore the graphics state to reset transformations; it's handled by resetTarget() in draw.c, called during the following + dp.Context = newContext(rt); + + loadAreaSize(a, rt, &(dp.AreaWidth), &(dp.AreaHeight)); + + dp.ClipX = clip->left; + dp.ClipY = clip->top; + dp.ClipWidth = clip->right - clip->left; + dp.ClipHeight = clip->bottom - clip->top; + if (a->scrolling) { + dp.ClipX += a->hscrollpos; + dp.ClipY += a->vscrollpos; + } + + rt->BeginDraw(); + + if (a->scrolling) { + ZeroMemory(&scrollTransform, sizeof (D2D1_MATRIX_3X2_F)); + scrollTransform._11 = 1; + scrollTransform._22 = 1; + // negative because we want nonzero scroll positions to move the drawing area up/left + scrollTransform._31 = -a->hscrollpos; + scrollTransform._32 = -a->vscrollpos; + rt->SetTransform(&scrollTransform); + } + + // TODO push axis aligned clip + + // TODO only clear the clip area + // TODO clear with actual background brush + bgcolorref = GetSysColor(COLOR_BTNFACE); + bgcolor.r = ((float) GetRValue(bgcolorref)) / 255.0; + // due to utter apathy on Microsoft's part, GetGValue() does not work with MSVC's Run-Time Error Checks + // it has not worked since 2008 and they have *never* fixed it + // TODO now that -RTCc has just been deprecated entirely, should we switch back? + bgcolor.g = ((float) ((BYTE) ((bgcolorref & 0xFF00) >> 8))) / 255.0; + bgcolor.b = ((float) GetBValue(bgcolorref)) / 255.0; + bgcolor.a = 1.0; + rt->Clear(&bgcolor); + + (*(ah->Draw))(ah, a, &dp); + + freeContext(dp.Context); + + // TODO pop axis aligned clip + + return rt->EndDraw(NULL, NULL); +} + +static void onWM_PAINT(uiArea *a) +{ + RECT clip; + HRESULT hr; + + // do not clear the update rect; we do that ourselves in doPaint() + if (GetUpdateRect(a->hwnd, &clip, FALSE) == 0) { + // set a zero clip rect just in case GetUpdateRect() didn't change clip + clip.left = 0; + clip.top = 0; + clip.right = 0; + clip.bottom = 0; + } + hr = doPaint(a, a->rt, &clip); + switch (hr) { + case S_OK: + if (ValidateRect(a->hwnd, NULL) == 0) + logLastError(L"error validating rect"); + break; + case D2DERR_RECREATE_TARGET: + // DON'T validate the rect + // instead, simply drop the render target + // we'll get another WM_PAINT and make the render target again + // TODO would this require us to invalidate the entire client area? + a->rt->Release();; + a->rt = NULL; + break; + default: + logHRESULT(L"error painting", hr); + } +} + +static void onWM_PRINTCLIENT(uiArea *a, HDC dc) +{ + ID2D1DCRenderTarget *rt; + RECT client; + HRESULT hr; + + uiWindowsEnsureGetClientRect(a->hwnd, &client); + rt = makeHDCRenderTarget(dc, &client); + hr = doPaint(a, rt, &client); + if (hr != S_OK) + logHRESULT(L"error printing uiArea client area", hr); + rt->Release(); +} + +BOOL areaDoDraw(uiArea *a, UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT *lResult) +{ + switch (uMsg) { + case WM_PAINT: + onWM_PAINT(a); + *lResult = 0; + return TRUE; + case WM_PRINTCLIENT: + onWM_PRINTCLIENT(a, (HDC) wParam); + *lResult = 0; + return TRUE; + } + return FALSE; +} + +// TODO only if the render target wasn't just created? +void areaDrawOnResize(uiArea *a, RECT *newClient) +{ + D2D1_SIZE_U size; + + size.width = newClient->right - newClient->left; + size.height = newClient->bottom - newClient->top; + // don't track the error; we'll get that in EndDraw() + // see https://msdn.microsoft.com/en-us/library/windows/desktop/dd370994%28v=vs.85%29.aspx + a->rt->Resize(&size); + + // according to Rick Brewster, we must always redraw the entire client area after calling ID2D1RenderTarget::Resize() (see http://stackoverflow.com/a/33222983/3408572) + // we used to have a uiAreaHandler.RedrawOnResize() method to decide this; now you know why we don't anymore + invalidateRect(a->hwnd, NULL, TRUE); +} diff --git a/src/libui_sdl/libui/windows/areaevents.cpp b/src/libui_sdl/libui/windows/areaevents.cpp new file mode 100644 index 0000000..7d391b8 --- /dev/null +++ b/src/libui_sdl/libui/windows/areaevents.cpp @@ -0,0 +1,421 @@ +// 8 september 2015 +#include "uipriv_windows.hpp" +#include "area.hpp" + +static uiModifiers getModifiers(void) +{ + uiModifiers m = 0; + + if ((GetKeyState(VK_CONTROL) & 0x80) != 0) + m |= uiModifierCtrl; + if ((GetKeyState(VK_MENU) & 0x80) != 0) + m |= uiModifierAlt; + if ((GetKeyState(VK_SHIFT) & 0x80) != 0) + m |= uiModifierShift; + if ((GetKeyState(VK_LWIN) & 0x80) != 0) + m |= uiModifierSuper; + if ((GetKeyState(VK_RWIN) & 0x80) != 0) + m |= uiModifierSuper; + return m; +} + +/* +Windows doesn't natively support mouse crossing events. + +TrackMouseEvent() (and its comctl32.dll wrapper _TrackMouseEvent()) both allow for a window to receive the WM_MOUSELEAVE message when the mouse leaves the client area. There's no equivalent WM_MOUSEENTER because it can be simulated (https://blogs.msdn.microsoft.com/oldnewthing/20031013-00/?p=42193). + +Unfortunately, WM_MOUSELEAVE does not get generated while the mouse is captured. We need to capture for drag behavior to work properly, so this isn't going to mix well. + +So what we do: +- on WM_MOUSEMOVE, if we don't have the capture, start tracking + - this will handle the case of the capture being released while still in the area +- on WM_MOUSELEAVE, mark that we are no longer tracking + - Windows has already done the work of that for us; it's just a flag we use for the next part +- when starting capture, stop tracking if we are tracking +- if capturing, manually check if the pointer is in the client rect on each area event +*/ +static void track(uiArea *a, BOOL tracking) +{ + TRACKMOUSEEVENT tm; + + // do nothing if there's no change + if (a->tracking && tracking) + return; + if (!a->tracking && !tracking) + return; + + a->tracking = tracking; + ZeroMemory(&tm, sizeof (TRACKMOUSEEVENT)); + tm.cbSize = sizeof (TRACKMOUSEEVENT); + tm.dwFlags = TME_LEAVE; + if (!a->tracking) + tm.dwFlags |= TME_CANCEL; + tm.hwndTrack = a->hwnd; + if (_TrackMouseEvent(&tm) == 0) + logLastError(L"error setting up mouse tracking"); +} + +static void capture(uiArea *a, BOOL capturing) +{ + // do nothing if there's no change + if (a->capturing && capturing) + return; + if (!a->capturing && !capturing) + return; + + // change flag first as ReleaseCapture() sends WM_CAPTURECHANGED + a->capturing = capturing; + if (a->capturing) { + track(a, FALSE); + SetCapture(a->hwnd); + } else + if (ReleaseCapture() == 0) + logLastError(L"error releasing capture on drag"); +} + +static void areaMouseEvent(uiArea *a, int down, int up, WPARAM wParam, LPARAM lParam) +{ + uiAreaMouseEvent me; + int button; + POINT clientpt; + RECT client; + BOOL inClient; + double xpix, ypix; + + if (a->capturing) { + clientpt.x = GET_X_LPARAM(lParam); + clientpt.y = GET_Y_LPARAM(lParam); + uiWindowsEnsureGetClientRect(a->hwnd, &client); + inClient = PtInRect(&client, clientpt); + if (inClient && !a->inside) { + a->inside = TRUE; + (*(a->ah->MouseCrossed))(a->ah, a, 0); + clickCounterReset(&(a->cc)); + } else if (!inClient && a->inside) { + a->inside = FALSE; + (*(a->ah->MouseCrossed))(a->ah, a, 1); + clickCounterReset(&(a->cc)); + } + } + + xpix = (double) GET_X_LPARAM(lParam); + ypix = (double) GET_Y_LPARAM(lParam); + // these are in pixels; we need points + pixelsToDIP(a, &xpix, &ypix); + me.X = xpix; + me.Y = ypix; + if (a->scrolling) { + me.X += a->hscrollpos; + me.Y += a->vscrollpos; + } + + loadAreaSize(a, NULL, &(me.AreaWidth), &(me.AreaHeight)); + + me.Down = down; + me.Up = up; + me.Count = 0; + if (me.Down != 0) + // GetMessageTime() returns LONG and GetDoubleClckTime() returns UINT, which are int32 and uint32, respectively, but we don't need to worry about the signedness because for the same bit widths and two's complement arithmetic, s1-s2 == u1-u2 if bits(s1)==bits(s2) and bits(u1)==bits(u2) (and Windows requires two's complement: http://blogs.msdn.com/b/oldnewthing/archive/2005/05/27/422551.aspx) + // signedness isn't much of an issue for these calls anyway because http://stackoverflow.com/questions/24022225/what-are-the-sign-extension-rules-for-calling-windows-api-functions-stdcall-t and that we're only using unsigned values (think back to how you (didn't) handle signedness in assembly language) AND because of the above AND because the statistics below (time interval and width/height) really don't make sense if negative + // GetSystemMetrics() returns int, which is int32 + me.Count = clickCounterClick(&(a->cc), me.Down, + me.X, me.Y, + GetMessageTime(), GetDoubleClickTime(), + GetSystemMetrics(SM_CXDOUBLECLK) / 2, + GetSystemMetrics(SM_CYDOUBLECLK) / 2); + + // though wparam will contain control and shift state, let's just one function to get modifiers for both keyboard and mouse events; it'll work the same anyway since we have to do this for alt and windows key (super) + me.Modifiers = getModifiers(); + + button = me.Down; + if (button == 0) + button = me.Up; + me.Held1To64 = 0; + if (button != 1 && (wParam & MK_LBUTTON) != 0) + me.Held1To64 |= 1 << 0; + if (button != 2 && (wParam & MK_MBUTTON) != 0) + me.Held1To64 |= 1 << 1; + if (button != 3 && (wParam & MK_RBUTTON) != 0) + me.Held1To64 |= 1 << 2; + if (button != 4 && (wParam & MK_XBUTTON1) != 0) + me.Held1To64 |= 1 << 3; + if (button != 5 && (wParam & MK_XBUTTON2) != 0) + me.Held1To64 |= 1 << 4; + + // on Windows, we have to capture on drag ourselves + if (me.Down != 0) + capture(a, TRUE); + // only release capture when all buttons released + if (me.Up != 0 && me.Held1To64 == 0) + capture(a, FALSE); + + (*(a->ah->MouseEvent))(a->ah, a, &me); +} + +// TODO genericize this so it can be called above +static void onMouseEntered(uiArea *a) +{ + if (a->inside) + return; + if (a->capturing) // we handle mouse crossing in areaMouseEvent() + return; + track(a, TRUE); + (*(a->ah->MouseCrossed))(a->ah, a, 0); + // TODO figure out why we did this to begin with; either we do it on both GTK+ and Windows or not at all + clickCounterReset(&(a->cc)); +} + +// TODO genericize it so that it can be called above +static void onMouseLeft(uiArea *a) +{ + a->tracking = FALSE; + a->inside = FALSE; + (*(a->ah->MouseCrossed))(a->ah, a, 1); + // TODO figure out why we did this to begin with; either we do it on both GTK+ and Windows or not at all + clickCounterReset(&(a->cc)); +} + +// we use VK_SNAPSHOT as a sentinel because libui will never support the print screen key; that key belongs to the user +struct extkeymap { + WPARAM vk; + uiExtKey extkey; +}; + +// all mappings come from GLFW - https://github.com/glfw/glfw/blob/master/src/win32_window.c#L152 +static const struct extkeymap numpadExtKeys[] = { + { VK_HOME, uiExtKeyN7 }, + { VK_UP, uiExtKeyN8 }, + { VK_PRIOR, uiExtKeyN9 }, + { VK_LEFT, uiExtKeyN4 }, + { VK_CLEAR, uiExtKeyN5 }, + { VK_RIGHT, uiExtKeyN6 }, + { VK_END, uiExtKeyN1 }, + { VK_DOWN, uiExtKeyN2 }, + { VK_NEXT, uiExtKeyN3 }, + { VK_INSERT, uiExtKeyN0 }, + { VK_DELETE, uiExtKeyNDot }, + { VK_SNAPSHOT, 0 }, +}; + +static const struct extkeymap extKeys[] = { + { VK_ESCAPE, uiExtKeyEscape }, + { VK_INSERT, uiExtKeyInsert }, + { VK_DELETE, uiExtKeyDelete }, + { VK_HOME, uiExtKeyHome }, + { VK_END, uiExtKeyEnd }, + { VK_PRIOR, uiExtKeyPageUp }, + { VK_NEXT, uiExtKeyPageDown }, + { VK_UP, uiExtKeyUp }, + { VK_DOWN, uiExtKeyDown }, + { VK_LEFT, uiExtKeyLeft }, + { VK_RIGHT, uiExtKeyRight }, + { VK_F1, uiExtKeyF1 }, + { VK_F2, uiExtKeyF2 }, + { VK_F3, uiExtKeyF3 }, + { VK_F4, uiExtKeyF4 }, + { VK_F5, uiExtKeyF5 }, + { VK_F6, uiExtKeyF6 }, + { VK_F7, uiExtKeyF7 }, + { VK_F8, uiExtKeyF8 }, + { VK_F9, uiExtKeyF9 }, + { VK_F10, uiExtKeyF10 }, + { VK_F11, uiExtKeyF11 }, + { VK_F12, uiExtKeyF12 }, + // numpad numeric keys and . are handled in common/areaevents.c + // numpad enter is handled in code below + { VK_ADD, uiExtKeyNAdd }, + { VK_SUBTRACT, uiExtKeyNSubtract }, + { VK_MULTIPLY, uiExtKeyNMultiply }, + { VK_DIVIDE, uiExtKeyNDivide }, + { VK_SNAPSHOT, 0 }, +}; + +static const struct { + WPARAM vk; + uiModifiers mod; +} modKeys[] = { + // even if the separate left/right aren't necessary, have them here anyway, just to be safe + { VK_CONTROL, uiModifierCtrl }, + { VK_LCONTROL, uiModifierCtrl }, + { VK_RCONTROL, uiModifierCtrl }, + { VK_MENU, uiModifierAlt }, + { VK_LMENU, uiModifierAlt }, + { VK_RMENU, uiModifierAlt }, + { VK_SHIFT, uiModifierShift }, + { VK_LSHIFT, uiModifierShift }, + { VK_RSHIFT, uiModifierShift }, + // there's no combined Windows key virtual-key code as there is with the others + { VK_LWIN, uiModifierSuper }, + { VK_RWIN, uiModifierSuper }, + { VK_SNAPSHOT, 0 }, +}; + +static int areaKeyEvent(uiArea *a, int up, WPARAM wParam, LPARAM lParam) +{ + uiAreaKeyEvent ke; + int righthand; + int i; + + ke.Key = 0; + ke.ExtKey = 0; + ke.Modifier = 0; + + ke.Modifiers = getModifiers(); + + ke.Up = up; + + // the numeric keypad keys when Num Lock is off are considered left-hand keys as the separate navigation buttons were added later + // the numeric keypad Enter, however, is a right-hand key because it has the same virtual-key code as the typewriter Enter + righthand = (lParam & 0x01000000) != 0; + if (righthand) { + if (wParam == VK_RETURN) { + ke.ExtKey = uiExtKeyNEnter; + goto keyFound; + } + } else + // this is special handling for numpad keys to ignore the state of Num Lock and Shift; see http://blogs.msdn.com/b/oldnewthing/archive/2004/09/06/226045.aspx and https://github.com/glfw/glfw/blob/master/src/win32_window.c#L152 + for (i = 0; numpadExtKeys[i].vk != VK_SNAPSHOT; i++) + if (numpadExtKeys[i].vk == wParam) { + ke.ExtKey = numpadExtKeys[i].extkey; + goto keyFound; + } + + // okay, those above cases didn't match anything + // first try the extended keys + for (i = 0; extKeys[i].vk != VK_SNAPSHOT; i++) + if (extKeys[i].vk == wParam) { + ke.ExtKey = extKeys[i].extkey; + goto keyFound; + } + + // then try modifier keys + for (i = 0; modKeys[i].vk != VK_SNAPSHOT; i++) + if (modKeys[i].vk == wParam) { + ke.Modifier = modKeys[i].mod; + // and don't include the key in Modifiers + ke.Modifiers &= ~ke.Modifier; + goto keyFound; + } + + // and finally everything else + if (fromScancode((lParam >> 16) & 0xFF, &ke)) + goto keyFound; + + // not a supported key, assume unhandled + // TODO the original code only did this if ke.Modifiers == 0 - why? + return 0; + +keyFound: + return (*(a->ah->KeyEvent))(a->ah, a, &ke); +} + +// We don't handle the standard Windows keyboard messages directly, to avoid both the dialog manager and TranslateMessage(). +// Instead, we set up a message filter and do things there. +// That stuff is later in this file. +enum { + // start at 0x40 to avoid clobbering dialog messages + msgAreaKeyDown = WM_USER + 0x40, + msgAreaKeyUp, +}; + +BOOL areaDoEvents(uiArea *a, UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT *lResult) +{ + switch (uMsg) { + case WM_ACTIVATE: + // don't keep the double-click timer running if the user switched programs in between clicks + clickCounterReset(&(a->cc)); + *lResult = 0; + return TRUE; + case WM_MOUSEMOVE: + onMouseEntered(a); + areaMouseEvent(a, 0, 0, wParam, lParam); + *lResult = 0; + return TRUE; + case WM_MOUSELEAVE: + onMouseLeft(a); + *lResult = 0; + return TRUE; + case WM_LBUTTONDOWN: + SetFocus(a->hwnd); + areaMouseEvent(a, 1, 0, wParam, lParam); + *lResult = 0; + return TRUE; + case WM_LBUTTONUP: + areaMouseEvent(a, 0, 1, wParam, lParam); + *lResult = 0; + return TRUE; + case WM_MBUTTONDOWN: + SetFocus(a->hwnd); + areaMouseEvent(a, 2, 0, wParam, lParam); + *lResult = 0; + return TRUE; + case WM_MBUTTONUP: + areaMouseEvent(a, 0, 2, wParam, lParam); + *lResult = 0; + return TRUE; + case WM_RBUTTONDOWN: + SetFocus(a->hwnd); + areaMouseEvent(a, 3, 0, wParam, lParam); + *lResult = 0; + return TRUE; + case WM_RBUTTONUP: + areaMouseEvent(a, 0, 3, wParam, lParam); + *lResult = 0; + return TRUE; + case WM_XBUTTONDOWN: + SetFocus(a->hwnd); + // values start at 1; we want them to start at 4 + areaMouseEvent(a, + GET_XBUTTON_WPARAM(wParam) + 3, 0, + GET_KEYSTATE_WPARAM(wParam), lParam); + *lResult = TRUE; // XBUTTON messages are different! + return TRUE; + case WM_XBUTTONUP: + areaMouseEvent(a, + 0, GET_XBUTTON_WPARAM(wParam) + 3, + GET_KEYSTATE_WPARAM(wParam), lParam); + *lResult = TRUE; // XBUTTON messages are different! + return TRUE; + case WM_CAPTURECHANGED: + if (a->capturing) { + a->capturing = FALSE; + (*(a->ah->DragBroken))(a->ah, a); + } + *lResult = 0; + return TRUE; + case msgAreaKeyDown: + *lResult = (LRESULT) areaKeyEvent(a, 0, wParam, lParam); + return TRUE; + case msgAreaKeyUp: + *lResult = (LRESULT) areaKeyEvent(a, 1, wParam, lParam); + return TRUE; + } + return FALSE; +} + +// TODO affect visibility properly +// TODO what did this mean +BOOL areaFilter(MSG *msg) +{ + LRESULT handled; + + // is the recipient an area? + if (msg->hwnd == NULL) // this can happen; for example, WM_TIMER + return FALSE; + if (windowClassOf(msg->hwnd, areaClass, NULL) != 0) + return FALSE; // nope + + handled = 0; + switch (msg->message) { + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + handled = SendMessageW(msg->hwnd, msgAreaKeyDown, msg->wParam, msg->lParam); + break; + case WM_KEYUP: + case WM_SYSKEYUP: + handled = SendMessageW(msg->hwnd, msgAreaKeyUp, msg->wParam, msg->lParam); + break; + // otherwise handled remains 0, as we didn't handle this + } + return (BOOL) handled; +} diff --git a/src/libui_sdl/libui/windows/areascroll.cpp b/src/libui_sdl/libui/windows/areascroll.cpp new file mode 100644 index 0000000..f18d0ad --- /dev/null +++ b/src/libui_sdl/libui/windows/areascroll.cpp @@ -0,0 +1,247 @@ +// 8 september 2015 +#include "uipriv_windows.hpp" +#include "area.hpp" + +// TODO +// - move from pixels to points somehow +// - add a function to offset points and rects by scrolling amounts; call it from doPaint() in areadraw.c +// - recalculate scrolling after: +// - creation? +// - resize? +// - recreating the render target? (after moving to points) +// - error if these are called without scrollbars? + +struct scrollParams { + int *pos; + int pagesize; + int length; + int *wheelCarry; + UINT wheelSPIAction; +}; + +static void scrollto(uiArea *a, int which, struct scrollParams *p, int pos) +{ + SCROLLINFO si; + + // note that the pos < 0 check is /after/ the p->length - p->pagesize check + // it used to be /before/; this was actually a bug in Raymond Chen's original algorithm: if there are fewer than a page's worth of items, p->length - p->pagesize will be negative and our content draw at the bottom of the window + // this SHOULD have the same effect with that bug fixed and no others introduced... (thanks to devin on irc.badnik.net for confirming this logic) + if (pos > p->length - p->pagesize) + pos = p->length - p->pagesize; + if (pos < 0) + pos = 0; + + // Direct2D doesn't have a method for scrolling the existing contents of a render target. + // We'll have to just invalidate everything and hope for the best. + invalidateRect(a->hwnd, NULL, FALSE); + + *(p->pos) = pos; + + // now commit our new scrollbar setup... + ZeroMemory(&si, sizeof (SCROLLINFO)); + si.cbSize = sizeof (SCROLLINFO); + si.fMask = SIF_PAGE | SIF_POS | SIF_RANGE; + si.nPage = p->pagesize; + si.nMin = 0; + si.nMax = p->length - 1; // endpoint inclusive + si.nPos = *(p->pos); + SetScrollInfo(a->hwnd, which, &si, TRUE); +} + +static void scrollby(uiArea *a, int which, struct scrollParams *p, int delta) +{ + scrollto(a, which, p, *(p->pos) + delta); +} + +static void scroll(uiArea *a, int which, struct scrollParams *p, WPARAM wParam, LPARAM lParam) +{ + int pos; + SCROLLINFO si; + + pos = *(p->pos); + switch (LOWORD(wParam)) { + case SB_LEFT: // also SB_TOP + pos = 0; + break; + case SB_RIGHT: // also SB_BOTTOM + pos = p->length - p->pagesize; + break; + case SB_LINELEFT: // also SB_LINEUP + pos--; + break; + case SB_LINERIGHT: // also SB_LINEDOWN + pos++; + break; + case SB_PAGELEFT: // also SB_PAGEUP + pos -= p->pagesize; + break; + case SB_PAGERIGHT: // also SB_PAGEDOWN + pos += p->pagesize; + break; + case SB_THUMBPOSITION: + ZeroMemory(&si, sizeof (SCROLLINFO)); + si.cbSize = sizeof (SCROLLINFO); + si.fMask = SIF_POS; + if (GetScrollInfo(a->hwnd, which, &si) == 0) + logLastError(L"error getting thumb position for area"); + pos = si.nPos; + break; + case SB_THUMBTRACK: + ZeroMemory(&si, sizeof (SCROLLINFO)); + si.cbSize = sizeof (SCROLLINFO); + si.fMask = SIF_TRACKPOS; + if (GetScrollInfo(a->hwnd, which, &si) == 0) + logLastError(L"error getting thumb track position for area"); + pos = si.nTrackPos; + break; + } + scrollto(a, which, p, pos); +} + +static void wheelscroll(uiArea *a, int which, struct scrollParams *p, WPARAM wParam, LPARAM lParam) +{ + int delta; + int lines; + UINT scrollAmount; + + delta = GET_WHEEL_DELTA_WPARAM(wParam); + if (SystemParametersInfoW(p->wheelSPIAction, 0, &scrollAmount, 0) == 0) + // TODO use scrollAmount == 3 (for both v and h) instead? + logLastError(L"error getting area wheel scroll amount"); + if (scrollAmount == WHEEL_PAGESCROLL) + scrollAmount = p->pagesize; + if (scrollAmount == 0) // no mouse wheel scrolling (or t->pagesize == 0) + return; + // the rest of this is basically http://blogs.msdn.com/b/oldnewthing/archive/2003/08/07/54615.aspx and http://blogs.msdn.com/b/oldnewthing/archive/2003/08/11/54624.aspx + // see those pages for information on subtleties + delta += *(p->wheelCarry); + lines = delta * ((int) scrollAmount) / WHEEL_DELTA; + *(p->wheelCarry) = delta - lines * WHEEL_DELTA / ((int) scrollAmount); + scrollby(a, which, p, -lines); +} + +static void hscrollParams(uiArea *a, struct scrollParams *p) +{ + RECT r; + + ZeroMemory(p, sizeof (struct scrollParams)); + p->pos = &(a->hscrollpos); + // TODO get rid of these and replace with points + uiWindowsEnsureGetClientRect(a->hwnd, &r); + p->pagesize = r.right - r.left; + p->length = a->scrollWidth; + p->wheelCarry = &(a->hwheelCarry); + p->wheelSPIAction = SPI_GETWHEELSCROLLCHARS; +} + +static void hscrollto(uiArea *a, int pos) +{ + struct scrollParams p; + + hscrollParams(a, &p); + scrollto(a, SB_HORZ, &p, pos); +} + +static void hscrollby(uiArea *a, int delta) +{ + struct scrollParams p; + + hscrollParams(a, &p); + scrollby(a, SB_HORZ, &p, delta); +} + +static void hscroll(uiArea *a, WPARAM wParam, LPARAM lParam) +{ + struct scrollParams p; + + hscrollParams(a, &p); + scroll(a, SB_HORZ, &p, wParam, lParam); +} + +static void hwheelscroll(uiArea *a, WPARAM wParam, LPARAM lParam) +{ + struct scrollParams p; + + hscrollParams(a, &p); + wheelscroll(a, SB_HORZ, &p, wParam, lParam); +} + +static void vscrollParams(uiArea *a, struct scrollParams *p) +{ + RECT r; + + ZeroMemory(p, sizeof (struct scrollParams)); + p->pos = &(a->vscrollpos); + uiWindowsEnsureGetClientRect(a->hwnd, &r); + p->pagesize = r.bottom - r.top; + p->length = a->scrollHeight; + p->wheelCarry = &(a->vwheelCarry); + p->wheelSPIAction = SPI_GETWHEELSCROLLLINES; +} + +static void vscrollto(uiArea *a, int pos) +{ + struct scrollParams p; + + vscrollParams(a, &p); + scrollto(a, SB_VERT, &p, pos); +} + +static void vscrollby(uiArea *a, int delta) +{ + struct scrollParams p; + + vscrollParams(a, &p); + scrollby(a, SB_VERT, &p, delta); +} + +static void vscroll(uiArea *a, WPARAM wParam, LPARAM lParam) +{ + struct scrollParams p; + + vscrollParams(a, &p); + scroll(a, SB_VERT, &p, wParam, lParam); +} + +static void vwheelscroll(uiArea *a, WPARAM wParam, LPARAM lParam) +{ + struct scrollParams p; + + vscrollParams(a, &p); + wheelscroll(a, SB_VERT, &p, wParam, lParam); +} + +BOOL areaDoScroll(uiArea *a, UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT *lResult) +{ + switch (uMsg) { + case WM_HSCROLL: + hscroll(a, wParam, lParam); + *lResult = 0; + return TRUE; + case WM_MOUSEHWHEEL: + hwheelscroll(a, wParam, lParam); + *lResult = 0; + return TRUE; + case WM_VSCROLL: + vscroll(a, wParam, lParam); + *lResult = 0; + return TRUE; + case WM_MOUSEWHEEL: + vwheelscroll(a, wParam, lParam); + *lResult = 0; + return TRUE; + } + return FALSE; +} + +void areaScrollOnResize(uiArea *a, RECT *client) +{ + areaUpdateScroll(a); +} + +void areaUpdateScroll(uiArea *a) +{ + // use a no-op scroll to simulate scrolling + hscrollby(a, 0); + vscrollby(a, 0); +} diff --git a/src/libui_sdl/libui/windows/areautil.cpp b/src/libui_sdl/libui/windows/areautil.cpp new file mode 100644 index 0000000..9dc72fb --- /dev/null +++ b/src/libui_sdl/libui/windows/areautil.cpp @@ -0,0 +1,37 @@ +// 18 december 2015 +#include "uipriv_windows.hpp" +#include "area.hpp" + +void loadAreaSize(uiArea *a, ID2D1RenderTarget *rt, double *width, double *height) +{ + D2D1_SIZE_F size; + + *width = 0; + *height = 0; + if (!a->scrolling) { + if (rt == NULL) + rt = a->rt; + size = realGetSize(rt); + *width = size.width; + *height = size.height; + } +} + +void pixelsToDIP(uiArea *a, double *x, double *y) +{ + FLOAT dpix, dpiy; + + a->rt->GetDpi(&dpix, &dpiy); + // see https://msdn.microsoft.com/en-us/library/windows/desktop/dd756649%28v=vs.85%29.aspx (and others; search "direct2d mouse") + *x = (*x * 96) / dpix; + *y = (*y * 96) / dpiy; +} + +void dipToPixels(uiArea *a, double *x, double *y) +{ + FLOAT dpix, dpiy; + + a->rt->GetDpi(&dpix, &dpiy); + *x = (*x * dpix) / 96; + *y = (*y * dpiy) / 96; +} diff --git a/src/libui_sdl/libui/windows/box.cpp b/src/libui_sdl/libui/windows/box.cpp new file mode 100644 index 0000000..9567954 --- /dev/null +++ b/src/libui_sdl/libui/windows/box.cpp @@ -0,0 +1,320 @@ +// 7 april 2015 +#include "uipriv_windows.hpp" + +struct boxChild { + uiControl *c; + int stretchy; + int width; + int height; +}; + +struct uiBox { + uiWindowsControl c; + HWND hwnd; + std::vector<struct boxChild> *controls; + int vertical; + int padded; +}; + +static void boxPadding(uiBox *b, int *xpadding, int *ypadding) +{ + uiWindowsSizing sizing; + + *xpadding = 0; + *ypadding = 0; + if (b->padded) { + uiWindowsGetSizing(b->hwnd, &sizing); + uiWindowsSizingStandardPadding(&sizing, xpadding, ypadding); + } +} + +static void boxRelayout(uiBox *b) +{ + RECT r; + int x, y, width, height; + int xpadding, ypadding; + int nStretchy; + int stretchywid, stretchyht; + int i; + int minimumWidth, minimumHeight; + int nVisible; + uiWindowsSizing *d; + + if (b->controls->size() == 0) + return; + + uiWindowsEnsureGetClientRect(b->hwnd, &r); + x = r.left; + y = r.top; + width = r.right - r.left; + height = r.bottom - r.top; + + // -1) get this Box's padding + boxPadding(b, &xpadding, &ypadding); + + // 1) get width and height of non-stretchy controls + // this will tell us how much space will be left for stretchy controls + stretchywid = width; + stretchyht = height; + nStretchy = 0; + nVisible = 0; + for (struct boxChild &bc : *(b->controls)) { + if (!uiControlVisible(bc.c)) + continue; + nVisible++; + if (bc.stretchy) { + nStretchy++; + continue; + } + uiWindowsControlMinimumSize(uiWindowsControl(bc.c), &minimumWidth, &minimumHeight); + if (b->vertical) { // all controls have same width + bc.width = width; + bc.height = minimumHeight; + stretchyht -= minimumHeight; + } else { // all controls have same height + bc.width = minimumWidth; + bc.height = height; + stretchywid -= minimumWidth; + } + } + if (nVisible == 0) // nothing to do + return; + + // 2) now inset the available rect by the needed padding + if (b->vertical) { + height -= (nVisible - 1) * ypadding; + stretchyht -= (nVisible - 1) * ypadding; + } else { + width -= (nVisible - 1) * xpadding; + stretchywid -= (nVisible - 1) * xpadding; + } + + // 3) now get the size of stretchy controls + if (nStretchy != 0) { + if (b->vertical) + stretchyht /= nStretchy; + else + stretchywid /= nStretchy; + for (struct boxChild &bc : *(b->controls)) { + if (!uiControlVisible(bc.c)) + continue; + if (bc.stretchy) { + bc.width = stretchywid; + bc.height = stretchyht; + } + } + } + + // 4) now we can position controls + // first, make relative to the top-left corner of the container + x = 0; + y = 0; + for (const struct boxChild &bc : *(b->controls)) { + if (!uiControlVisible(bc.c)) + continue; + uiWindowsEnsureMoveWindowDuringResize((HWND) uiControlHandle(bc.c), x, y, bc.width, bc.height); + if (b->vertical) + y += bc.height + ypadding; + else + x += bc.width + xpadding; + } +} + +static void uiBoxDestroy(uiControl *c) +{ + uiBox *b = uiBox(c); + + for (const struct boxChild &bc : *(b->controls)) { + uiControlSetParent(bc.c, NULL); + uiControlDestroy(bc.c); + } + delete b->controls; + uiWindowsEnsureDestroyWindow(b->hwnd); + uiFreeControl(uiControl(b)); +} + +uiWindowsControlDefaultHandle(uiBox) +uiWindowsControlDefaultParent(uiBox) +uiWindowsControlDefaultSetParent(uiBox) +uiWindowsControlDefaultToplevel(uiBox) +uiWindowsControlDefaultVisible(uiBox) +uiWindowsControlDefaultShow(uiBox) +uiWindowsControlDefaultHide(uiBox) +uiWindowsControlDefaultEnabled(uiBox) +uiWindowsControlDefaultEnable(uiBox) +uiWindowsControlDefaultDisable(uiBox) + +static void uiBoxSyncEnableState(uiWindowsControl *c, int enabled) +{ + uiBox *b = uiBox(c); + + if (uiWindowsShouldStopSyncEnableState(uiWindowsControl(b), enabled)) + return; + for (const struct boxChild &bc : *(b->controls)) + uiWindowsControlSyncEnableState(uiWindowsControl(bc.c), enabled); +} + +uiWindowsControlDefaultSetParentHWND(uiBox) + +static void uiBoxMinimumSize(uiWindowsControl *c, int *width, int *height) +{ + uiBox *b = uiBox(c); + int xpadding, ypadding; + int nStretchy; + // these two contain the largest minimum width and height of all stretchy controls in the box + // all stretchy controls will use this value to determine the final minimum size + int maxStretchyWidth, maxStretchyHeight; + int i; + int minimumWidth, minimumHeight; + int nVisible; + uiWindowsSizing sizing; + + *width = 0; + *height = 0; + if (b->controls->size() == 0) + return; + + // 0) get this Box's padding + boxPadding(b, &xpadding, &ypadding); + + // 1) add in the size of non-stretchy controls and get (but not add in) the largest widths and heights of stretchy controls + // we still add in like direction of stretchy controls + nStretchy = 0; + maxStretchyWidth = 0; + maxStretchyHeight = 0; + nVisible = 0; + for (const struct boxChild &bc : *(b->controls)) { + if (!uiControlVisible(bc.c)) + continue; + nVisible++; + uiWindowsControlMinimumSize(uiWindowsControl(bc.c), &minimumWidth, &minimumHeight); + if (bc.stretchy) { + nStretchy++; + if (maxStretchyWidth < minimumWidth) + maxStretchyWidth = minimumWidth; + if (maxStretchyHeight < minimumHeight) + maxStretchyHeight = minimumHeight; + } + if (b->vertical) { + if (*width < minimumWidth) + *width = minimumWidth; + if (!bc.stretchy) + *height += minimumHeight; + } else { + if (!bc.stretchy) + *width += minimumWidth; + if (*height < minimumHeight) + *height = minimumHeight; + } + } + if (nVisible == 0) // just return 0x0 + return; + + // 2) now outset the desired rect with the needed padding + if (b->vertical) + *height += (nVisible - 1) * ypadding; + else + *width += (nVisible - 1) * xpadding; + + // 3) and now we can add in stretchy controls + if (b->vertical) + *height += nStretchy * maxStretchyHeight; + else + *width += nStretchy * maxStretchyWidth; +} + +static void uiBoxMinimumSizeChanged(uiWindowsControl *c) +{ + uiBox *b = uiBox(c); + + if (uiWindowsControlTooSmall(uiWindowsControl(b))) { + uiWindowsControlContinueMinimumSizeChanged(uiWindowsControl(b)); + return; + } + boxRelayout(b); +} + +uiWindowsControlDefaultLayoutRect(uiBox) +uiWindowsControlDefaultAssignControlIDZOrder(uiBox) + +static void uiBoxChildVisibilityChanged(uiWindowsControl *c) +{ + // TODO eliminate the redundancy + uiWindowsControlMinimumSizeChanged(c); +} + +static void boxArrangeChildren(uiBox *b) +{ + LONG_PTR controlID; + HWND insertAfter; + + controlID = 100; + insertAfter = NULL; + for (const struct boxChild &bc : *(b->controls)) + uiWindowsControlAssignControlIDZOrder(uiWindowsControl(bc.c), &controlID, &insertAfter); +} + +void uiBoxAppend(uiBox *b, uiControl *c, int stretchy) +{ + struct boxChild bc; + + bc.c = c; + bc.stretchy = stretchy; + uiControlSetParent(bc.c, uiControl(b)); + uiWindowsControlSetParentHWND(uiWindowsControl(bc.c), b->hwnd); + b->controls->push_back(bc); + boxArrangeChildren(b); + uiWindowsControlMinimumSizeChanged(uiWindowsControl(b)); +} + +void uiBoxDelete(uiBox *b, int index) +{ + uiControl *c; + + c = (*(b->controls))[index].c; + uiControlSetParent(c, NULL); + uiWindowsControlSetParentHWND(uiWindowsControl(c), NULL); + b->controls->erase(b->controls->begin() + index); + boxArrangeChildren(b); + uiWindowsControlMinimumSizeChanged(uiWindowsControl(b)); +} + +int uiBoxPadded(uiBox *b) +{ + return b->padded; +} + +void uiBoxSetPadded(uiBox *b, int padded) +{ + b->padded = padded; + uiWindowsControlMinimumSizeChanged(uiWindowsControl(b)); +} + +static void onResize(uiWindowsControl *c) +{ + boxRelayout(uiBox(c)); +} + +static uiBox *finishNewBox(int vertical) +{ + uiBox *b; + + uiWindowsNewControl(uiBox, b); + + b->hwnd = uiWindowsMakeContainer(uiWindowsControl(b), onResize); + + b->vertical = vertical; + b->controls = new std::vector<struct boxChild>; + + return b; +} + +uiBox *uiNewHorizontalBox(void) +{ + return finishNewBox(0); +} + +uiBox *uiNewVerticalBox(void) +{ + return finishNewBox(1); +} diff --git a/src/libui_sdl/libui/windows/button.cpp b/src/libui_sdl/libui/windows/button.cpp new file mode 100644 index 0000000..3b12e72 --- /dev/null +++ b/src/libui_sdl/libui/windows/button.cpp @@ -0,0 +1,104 @@ +// 7 april 2015 +#include "uipriv_windows.hpp" + +struct uiButton { + uiWindowsControl c; + HWND hwnd; + void (*onClicked)(uiButton *, void *); + void *onClickedData; +}; + +static BOOL onWM_COMMAND(uiControl *c, HWND hwnd, WORD code, LRESULT *lResult) +{ + uiButton *b = uiButton(c); + + if (code != BN_CLICKED) + return FALSE; + (*(b->onClicked))(b, b->onClickedData); + *lResult = 0; + return TRUE; +} + +static void uiButtonDestroy(uiControl *c) +{ + uiButton *b = uiButton(c); + + uiWindowsUnregisterWM_COMMANDHandler(b->hwnd); + uiWindowsEnsureDestroyWindow(b->hwnd); + uiFreeControl(uiControl(b)); +} + +uiWindowsControlAllDefaultsExceptDestroy(uiButton) + +// from http://msdn.microsoft.com/en-us/library/windows/desktop/dn742486.aspx#sizingandspacing +#define buttonHeight 14 + +static void uiButtonMinimumSize(uiWindowsControl *c, int *width, int *height) +{ + uiButton *b = uiButton(c); + SIZE size; + uiWindowsSizing sizing; + int y; + + // try the comctl32 version 6 way + size.cx = 0; // explicitly ask for ideal size + size.cy = 0; + if (SendMessageW(b->hwnd, BCM_GETIDEALSIZE, 0, (LPARAM) (&size)) != FALSE) { + *width = size.cx; + *height = size.cy; + return; + } + + // that didn't work; fall back to using Microsoft's metrics + // Microsoft says to use a fixed width for all buttons; this isn't good enough + // use the text width instead, with some edge padding + *width = uiWindowsWindowTextWidth(b->hwnd) + (2 * GetSystemMetrics(SM_CXEDGE)); + y = buttonHeight; + uiWindowsGetSizing(b->hwnd, &sizing); + uiWindowsSizingDlgUnitsToPixels(&sizing, NULL, &y); + *height = y; +} + +static void defaultOnClicked(uiButton *b, void *data) +{ + // do nothing +} + +char *uiButtonText(uiButton *b) +{ + return uiWindowsWindowText(b->hwnd); +} + +void uiButtonSetText(uiButton *b, const char *text) +{ + uiWindowsSetWindowText(b->hwnd, text); + // changing the text might necessitate a change in the button's size + uiWindowsControlMinimumSizeChanged(uiWindowsControl(b)); +} + +void uiButtonOnClicked(uiButton *b, void (*f)(uiButton *, void *), void *data) +{ + b->onClicked = f; + b->onClickedData = data; +} + +uiButton *uiNewButton(const char *text) +{ + uiButton *b; + WCHAR *wtext; + + uiWindowsNewControl(uiButton, b); + + wtext = toUTF16(text); + b->hwnd = uiWindowsEnsureCreateControlHWND(0, + L"button", wtext, + BS_PUSHBUTTON | WS_TABSTOP, + hInstance, NULL, + TRUE); + uiFree(wtext); + + uiWindowsRegisterWM_COMMANDHandler(b->hwnd, onWM_COMMAND, uiControl(b)); + uiButtonOnClicked(b, defaultOnClicked, NULL); + + return b; +} diff --git a/src/libui_sdl/libui/windows/checkbox.cpp b/src/libui_sdl/libui/windows/checkbox.cpp new file mode 100644 index 0000000..be425c0 --- /dev/null +++ b/src/libui_sdl/libui/windows/checkbox.cpp @@ -0,0 +1,117 @@ +// 7 april 2015 +#include "uipriv_windows.hpp" + +struct uiCheckbox { + uiWindowsControl c; + HWND hwnd; + void (*onToggled)(uiCheckbox *, void *); + void *onToggledData; +}; + +static BOOL onWM_COMMAND(uiControl *cc, HWND hwnd, WORD code, LRESULT *lResult) +{ + uiCheckbox *c = uiCheckbox(cc); + WPARAM check; + + if (code != BN_CLICKED) + return FALSE; + + // we didn't use BS_AUTOCHECKBOX (http://blogs.msdn.com/b/oldnewthing/archive/2014/05/22/10527522.aspx) so we have to manage the check state ourselves + check = BST_CHECKED; + if (SendMessage(c->hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED) + check = BST_UNCHECKED; + SendMessage(c->hwnd, BM_SETCHECK, check, 0); + + (*(c->onToggled))(c, c->onToggledData); + *lResult = 0; + return TRUE; +} + +static void uiCheckboxDestroy(uiControl *cc) +{ + uiCheckbox *c = uiCheckbox(cc); + + uiWindowsUnregisterWM_COMMANDHandler(c->hwnd); + uiWindowsEnsureDestroyWindow(c->hwnd); + uiFreeControl(uiControl(c)); +} + +uiWindowsControlAllDefaultsExceptDestroy(uiCheckbox) + +// from http://msdn.microsoft.com/en-us/library/windows/desktop/dn742486.aspx#sizingandspacing +#define checkboxHeight 10 +// from http://msdn.microsoft.com/en-us/library/windows/desktop/bb226818%28v=vs.85%29.aspx +#define checkboxXFromLeftOfBoxToLeftOfLabel 12 + +static void uiCheckboxMinimumSize(uiWindowsControl *cc, int *width, int *height) +{ + uiCheckbox *c = uiCheckbox(cc); + uiWindowsSizing sizing; + int x, y; + + x = checkboxXFromLeftOfBoxToLeftOfLabel; + y = checkboxHeight; + uiWindowsGetSizing(c->hwnd, &sizing); + uiWindowsSizingDlgUnitsToPixels(&sizing, &x, &y); + *width = x + uiWindowsWindowTextWidth(c->hwnd); + *height = y; +} + +static void defaultOnToggled(uiCheckbox *c, void *data) +{ + // do nothing +} + +char *uiCheckboxText(uiCheckbox *c) +{ + return uiWindowsWindowText(c->hwnd); +} + +void uiCheckboxSetText(uiCheckbox *c, const char *text) +{ + uiWindowsSetWindowText(c->hwnd, text); + // changing the text might necessitate a change in the checkbox's size + uiWindowsControlMinimumSizeChanged(uiWindowsControl(c)); +} + +void uiCheckboxOnToggled(uiCheckbox *c, void (*f)(uiCheckbox *, void *), void *data) +{ + c->onToggled = f; + c->onToggledData = data; +} + +int uiCheckboxChecked(uiCheckbox *c) +{ + return SendMessage(c->hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED; +} + +void uiCheckboxSetChecked(uiCheckbox *c, int checked) +{ + WPARAM check; + + check = BST_CHECKED; + if (!checked) + check = BST_UNCHECKED; + SendMessage(c->hwnd, BM_SETCHECK, check, 0); +} + +uiCheckbox *uiNewCheckbox(const char *text) +{ + uiCheckbox *c; + WCHAR *wtext; + + uiWindowsNewControl(uiCheckbox, c); + + wtext = toUTF16(text); + c->hwnd = uiWindowsEnsureCreateControlHWND(0, + L"button", wtext, + BS_CHECKBOX | WS_TABSTOP, + hInstance, NULL, + TRUE); + uiFree(wtext); + + uiWindowsRegisterWM_COMMANDHandler(c->hwnd, onWM_COMMAND, uiControl(c)); + uiCheckboxOnToggled(c, defaultOnToggled, NULL); + + return c; +} diff --git a/src/libui_sdl/libui/windows/colorbutton.cpp b/src/libui_sdl/libui/windows/colorbutton.cpp new file mode 100644 index 0000000..c1ba695 --- /dev/null +++ b/src/libui_sdl/libui/windows/colorbutton.cpp @@ -0,0 +1,192 @@ +// 16 may 2016 +#include "uipriv_windows.hpp" + +struct uiColorButton { + uiWindowsControl c; + HWND hwnd; + double r; + double g; + double b; + double a; + void (*onChanged)(uiColorButton *, void *); + void *onChangedData; +}; + +static void uiColorButtonDestroy(uiControl *c) +{ + uiColorButton *b = uiColorButton(c); + + uiWindowsUnregisterWM_COMMANDHandler(b->hwnd); + uiWindowsUnregisterWM_NOTIFYHandler(b->hwnd); + uiWindowsEnsureDestroyWindow(b->hwnd); + uiFreeControl(uiControl(b)); +} + +static BOOL onWM_COMMAND(uiControl *c, HWND hwnd, WORD code, LRESULT *lResult) +{ + uiColorButton *b = uiColorButton(c); + HWND parent; + struct colorDialogRGBA rgba; + + if (code != BN_CLICKED) + return FALSE; + + parent = parentToplevel(b->hwnd); + rgba.r = b->r; + rgba.g = b->g; + rgba.b = b->b; + rgba.a = b->a; + if (showColorDialog(parent, &rgba)) { + b->r = rgba.r; + b->g = rgba.g; + b->b = rgba.b; + b->a = rgba.a; + invalidateRect(b->hwnd, NULL, TRUE); + (*(b->onChanged))(b, b->onChangedData); + } + + *lResult = 0; + return TRUE; +} + +static BOOL onWM_NOTIFY(uiControl *c, HWND hwnd, NMHDR *nmhdr, LRESULT *lResult) +{ + uiColorButton *b = uiColorButton(c); + NMCUSTOMDRAW *nm = (NMCUSTOMDRAW *) nmhdr; + RECT client; + ID2D1DCRenderTarget *rt; + D2D1_RECT_F r; + D2D1_COLOR_F color; + D2D1_BRUSH_PROPERTIES bprop; + ID2D1SolidColorBrush *brush; + uiWindowsSizing sizing; + int x, y; + HRESULT hr; + + if (nmhdr->code != NM_CUSTOMDRAW) + return FALSE; + // and allow the button to draw its background + if (nm->dwDrawStage != CDDS_PREPAINT) + return FALSE; + + uiWindowsEnsureGetClientRect(b->hwnd, &client); + rt = makeHDCRenderTarget(nm->hdc, &client); + rt->BeginDraw(); + + uiWindowsGetSizing(b->hwnd, &sizing); + x = 3; // should be enough + y = 3; + uiWindowsSizingDlgUnitsToPixels(&sizing, &x, &y); + r.left = client.left + x; + r.top = client.top + y; + r.right = client.right - x; + r.bottom = client.bottom - y; + + color.r = b->r; + color.g = b->g; + color.b = b->b; + color.a = b->a; + ZeroMemory(&bprop, sizeof (D2D1_BRUSH_PROPERTIES)); + bprop.opacity = 1.0; + bprop.transform._11 = 1; + bprop.transform._22 = 1; + hr = rt->CreateSolidColorBrush(&color, &bprop, &brush); + if (hr != S_OK) + logHRESULT(L"error creating brush for color button", hr); + rt->FillRectangle(&r, brush); + brush->Release(); + + hr = rt->EndDraw(NULL, NULL); + if (hr != S_OK) + logHRESULT(L"error drawing color on color button", hr); + rt->Release(); + + // skip default processing (don't draw text) + *lResult = CDRF_SKIPDEFAULT; + return TRUE; +} + +uiWindowsControlAllDefaultsExceptDestroy(uiColorButton) + +// from http://msdn.microsoft.com/en-us/library/windows/desktop/dn742486.aspx#sizingandspacing +#define buttonHeight 14 + +// TODO check widths +static void uiColorButtonMinimumSize(uiWindowsControl *c, int *width, int *height) +{ + uiColorButton *b = uiColorButton(c); + SIZE size; + uiWindowsSizing sizing; + int y; + + // try the comctl32 version 6 way + size.cx = 0; // explicitly ask for ideal size + size.cy = 0; + if (SendMessageW(b->hwnd, BCM_GETIDEALSIZE, 0, (LPARAM) (&size)) != FALSE) { + *width = size.cx; + *height = size.cy; + return; + } + + // that didn't work; fall back to using Microsoft's metrics + // Microsoft says to use a fixed width for all buttons; this isn't good enough + // use the text width instead, with some edge padding + *width = uiWindowsWindowTextWidth(b->hwnd) + (2 * GetSystemMetrics(SM_CXEDGE)); + y = buttonHeight; + uiWindowsGetSizing(b->hwnd, &sizing); + uiWindowsSizingDlgUnitsToPixels(&sizing, NULL, &y); + *height = y; +} + +static void defaultOnChanged(uiColorButton *b, void *data) +{ + // do nothing +} + +void uiColorButtonColor(uiColorButton *b, double *r, double *g, double *bl, double *a) +{ + *r = b->r; + *g = b->g; + *bl = b->b; + *a = b->a; +} + +void uiColorButtonSetColor(uiColorButton *b, double r, double g, double bl, double a) +{ + b->r = r; + b->g = g; + b->b = bl; + b->a = a; + invalidateRect(b->hwnd, NULL, TRUE); +} + +void uiColorButtonOnChanged(uiColorButton *b, void (*f)(uiColorButton *, void *), void *data) +{ + b->onChanged = f; + b->onChangedData = data; +} + +uiColorButton *uiNewColorButton(void) +{ + uiColorButton *b; + + uiWindowsNewControl(uiColorButton, b); + + // initial color is black + b->r = 0.0; + b->g = 0.0; + b->b = 0.0; + b->a = 1.0; + + b->hwnd = uiWindowsEnsureCreateControlHWND(0, + L"button", L" ", // TODO; can't use "" TODO + BS_PUSHBUTTON | WS_TABSTOP, + hInstance, NULL, + TRUE); + + uiWindowsRegisterWM_COMMANDHandler(b->hwnd, onWM_COMMAND, uiControl(b)); + uiWindowsRegisterWM_NOTIFYHandler(b->hwnd, onWM_NOTIFY, uiControl(b)); + uiColorButtonOnChanged(b, defaultOnChanged, NULL); + + return b; +} diff --git a/src/libui_sdl/libui/windows/colordialog.cpp b/src/libui_sdl/libui/windows/colordialog.cpp new file mode 100644 index 0000000..2efe72c --- /dev/null +++ b/src/libui_sdl/libui/windows/colordialog.cpp @@ -0,0 +1,1264 @@ +// 16 may 2016 +#include "uipriv_windows.hpp" + +// TODO should the d2dscratch programs capture mouse? + +struct colorDialog { + HWND hwnd; + + HWND svChooser; + HWND hSlider; + HWND preview; + HWND opacitySlider; + HWND editH; + HWND editS; + HWND editV; + HWND editRDouble, editRInt; + HWND editGDouble, editGInt; + HWND editBDouble, editBInt; + HWND editADouble, editAInt; + HWND editHex; + + double h; + double s; + double v; + double a; + struct colorDialogRGBA *out; + + BOOL updating; +}; + +// both of these are from the wikipedia page on HSV +// TODO what to do about negative h? +static void rgb2HSV(double r, double g, double b, double *h, double *s, double *v) +{ + double M, m; + int whichmax; + double c; + + M = r; + whichmax = 0; + if (M < g) { + M = g; + whichmax = 1; + } + if (M < b) { + M = b; + whichmax = 2; + } + m = r; + if (m > g) + m = g; + if (m > b) + m = b; + c = M - m; + + if (c == 0) + *h = 0; + else { + switch (whichmax) { + case 0: + *h = ((g - b) / c); + *h = fmod(*h, 6); + break; + case 1: + *h = ((b - r) / c) + 2; + break; + case 2: + *h = ((r - g) / c) + 4; + break; + } + *h /= 6; // put in range [0,1) + } + + *v = M; + + if (c == 0) + *s = 0; + else + *s = c / *v; +} + +// TODO negative R values? +static void hsv2RGB(double h, double s, double v, double *r, double *g, double *b) +{ + double c; + double hPrime; + int h60; + double x; + double m; + double c1, c2; + + c = v * s; + hPrime = h * 6; + h60 = (int) hPrime; // equivalent to splitting into 60° chunks + x = c * (1.0 - fabs(fmod(hPrime, 2) - 1.0)); + m = v - c; + switch (h60) { + case 0: + *r = c + m; + *g = x + m; + *b = m; + return; + case 1: + *r = x + m; + *g = c + m; + *b = m; + return; + case 2: + *r = m; + *g = c + m; + *b = x + m; + return; + case 3: + *r = m; + *g = x + m; + *b = c + m; + return; + case 4: + *r = x + m; + *g = m; + *b = c + m; + return; + case 5: + *r = c + m; + *g = m; + *b = x + m; + return; + } + // TODO +} + +#define hexd L"0123456789ABCDEF" + +static void rgba2Hex(uint8_t r, uint8_t g, uint8_t b, uint8_t a, WCHAR *buf) +{ + buf[0] = L'#'; + buf[1] = hexd[(a >> 4) & 0xF]; + buf[2] = hexd[a & 0xF]; + buf[3] = hexd[(r >> 4) & 0xF]; + buf[4] = hexd[r & 0xF]; + buf[5] = hexd[(g >> 4) & 0xF]; + buf[6] = hexd[g & 0xF]; + buf[7] = hexd[(b >> 4) & 0xF]; + buf[8] = hexd[b & 0xF]; + buf[9] = L'\0'; +} + +static int convHexDigit(WCHAR c) +{ + if (c >= L'0' && c <= L'9') + return c - L'0'; + if (c >= L'A' && c <= L'F') + return c - L'A' + 0xA; + if (c >= L'a' && c <= L'f') + return c - L'a' + 0xA; + return -1; +} + +// TODO allow #NNN shorthand +static BOOL hex2RGBA(WCHAR *buf, double *r, double *g, double *b, double *a) +{ + uint8_t component; + int i; + + if (*buf == L'#') + buf++; + + component = 0; + i = convHexDigit(*buf++); + if (i < 0) + return FALSE; + component |= ((uint8_t) i) << 4; + i = convHexDigit(*buf++); + if (i < 0) + return FALSE; + component |= ((uint8_t) i); + *a = ((double) component) / 255; + + component = 0; + i = convHexDigit(*buf++); + if (i < 0) + return FALSE; + component |= ((uint8_t) i) << 4; + i = convHexDigit(*buf++); + if (i < 0) + return FALSE; + component |= ((uint8_t) i); + *r = ((double) component) / 255; + + component = 0; + i = convHexDigit(*buf++); + if (i < 0) + return FALSE; + component |= ((uint8_t) i) << 4; + i = convHexDigit(*buf++); + if (i < 0) + return FALSE; + component |= ((uint8_t) i); + *g = ((double) component) / 255; + + if (*buf == L'\0') { // #NNNNNN syntax + *b = *g; + *g = *r; + *r = *a; + *a = 1; + return TRUE; + } + + component = 0; + i = convHexDigit(*buf++); + if (i < 0) + return FALSE; + component |= ((uint8_t) i) << 4; + i = convHexDigit(*buf++); + if (i < 0) + return FALSE; + component |= ((uint8_t) i); + *b = ((double) component) / 255; + + return *buf == L'\0'; +} + +static void updateDouble(HWND hwnd, double d, HWND whichChanged) +{ + WCHAR *str; + + if (whichChanged == hwnd) + return; + str = ftoutf16(d); + setWindowText(hwnd, str); + uiFree(str); +} + +static void updateDialog(struct colorDialog *c, HWND whichChanged) +{ + double r, g, b; + uint8_t rb, gb, bb, ab; + WCHAR *str; + WCHAR hexbuf[16]; // more than enough + + c->updating = TRUE; + + updateDouble(c->editH, c->h, whichChanged); + updateDouble(c->editS, c->s, whichChanged); + updateDouble(c->editV, c->v, whichChanged); + + hsv2RGB(c->h, c->s, c->v, &r, &g, &b); + + updateDouble(c->editRDouble, r, whichChanged); + updateDouble(c->editGDouble, g, whichChanged); + updateDouble(c->editBDouble, b, whichChanged); + updateDouble(c->editADouble, c->a, whichChanged); + + rb = (uint8_t) (r * 255); + gb = (uint8_t) (g * 255); + bb = (uint8_t) (b * 255); + ab = (uint8_t) (c->a * 255); + + if (whichChanged != c->editRInt) { + str = itoutf16(rb); + setWindowText(c->editRInt, str); + uiFree(str); + } + if (whichChanged != c->editGInt) { + str = itoutf16(gb); + setWindowText(c->editGInt, str); + uiFree(str); + } + if (whichChanged != c->editBInt) { + str = itoutf16(bb); + setWindowText(c->editBInt, str); + uiFree(str); + } + if (whichChanged != c->editAInt) { + str = itoutf16(ab); + setWindowText(c->editAInt, str); + uiFree(str); + } + + if (whichChanged != c->editHex) { + rgba2Hex(rb, gb, bb, ab, hexbuf); + setWindowText(c->editHex, hexbuf); + } + + // TODO TRUE? + invalidateRect(c->svChooser, NULL, TRUE); + invalidateRect(c->hSlider, NULL, TRUE); + invalidateRect(c->preview, NULL, TRUE); + invalidateRect(c->opacitySlider, NULL, TRUE); + + c->updating = FALSE; +} + +// this imitates http://blogs.msdn.com/b/wpfsdk/archive/2006/10/26/uncommon-dialogs--font-chooser-and-color-picker-dialogs.aspx +static void drawGrid(ID2D1RenderTarget *rt, D2D1_RECT_F *fillRect) +{ + D2D1_SIZE_F size; + D2D1_PIXEL_FORMAT pformat; + ID2D1BitmapRenderTarget *brt; + D2D1_COLOR_F color; + D2D1_BRUSH_PROPERTIES bprop; + ID2D1SolidColorBrush *brush; + D2D1_RECT_F rect; + ID2D1Bitmap *bitmap; + D2D1_BITMAP_BRUSH_PROPERTIES bbp; + ID2D1BitmapBrush *bb; + HRESULT hr; + + // mind the divisions; they represent the fact the original uses a viewport + size.width = 100 / 10; + size.height = 100 / 10; + // yay more ABI bugs +#ifdef _MSC_VER + pformat = rt->GetPixelFormat(); +#else + { + typedef D2D1_PIXEL_FORMAT *(__stdcall ID2D1RenderTarget::* GetPixelFormatF)(D2D1_PIXEL_FORMAT *); + GetPixelFormatF gpf; + + gpf = (GetPixelFormatF) (&(rt->GetPixelFormat)); + (rt->*gpf)(&pformat); + } +#endif + hr = rt->CreateCompatibleRenderTarget(&size, NULL, + &pformat, D2D1_COMPATIBLE_RENDER_TARGET_OPTIONS_NONE, + &brt); + if (hr != S_OK) + logHRESULT(L"error creating render target for grid", hr); + + brt->BeginDraw(); + + color.r = 1.0; + color.g = 1.0; + color.b = 1.0; + color.a = 1.0; + brt->Clear(&color); + + color = D2D1::ColorF(D2D1::ColorF::LightGray, 1.0); + ZeroMemory(&bprop, sizeof (D2D1_BRUSH_PROPERTIES)); + bprop.opacity = 1.0; + bprop.transform._11 = 1; + bprop.transform._22 = 1; + hr = brt->CreateSolidColorBrush(&color, &bprop, &brush); + if (hr != S_OK) + logHRESULT(L"error creating brush for grid", hr); + rect.left = 0; + rect.top = 0; + rect.right = 50 / 10; + rect.bottom = 50 / 10; + brt->FillRectangle(&rect, brush); + rect.left = 50 / 10; + rect.top = 50 / 10; + rect.right = 100 / 10; + rect.bottom = 100 / 10; + brt->FillRectangle(&rect, brush); + brush->Release(); + + hr = brt->EndDraw(NULL, NULL); + if (hr != S_OK) + logHRESULT(L"error finalizing render target for grid", hr); + hr = brt->GetBitmap(&bitmap); + if (hr != S_OK) + logHRESULT(L"error getting bitmap for grid", hr); + brt->Release(); + + ZeroMemory(&bbp, sizeof (D2D1_BITMAP_BRUSH_PROPERTIES)); + bbp.extendModeX = D2D1_EXTEND_MODE_WRAP; + bbp.extendModeY = D2D1_EXTEND_MODE_WRAP; + bbp.interpolationMode = D2D1_BITMAP_INTERPOLATION_MODE_NEAREST_NEIGHBOR; + hr = rt->CreateBitmapBrush(bitmap, &bbp, &bprop, &bb); + if (hr != S_OK) + logHRESULT(L"error creating bitmap brush for grid", hr); + rt->FillRectangle(fillRect, bb); + bb->Release(); + bitmap->Release(); +} + +// this interesting approach comes from http://blogs.msdn.com/b/wpfsdk/archive/2006/10/26/uncommon-dialogs--font-chooser-and-color-picker-dialogs.aspx +static void drawSVChooser(struct colorDialog *c, ID2D1RenderTarget *rt) +{ + D2D1_SIZE_F size; + D2D1_RECT_F rect; + double rTop, gTop, bTop; + D2D1_GRADIENT_STOP stops[2]; + ID2D1GradientStopCollection *collection; + D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES lprop; + D2D1_BRUSH_PROPERTIES bprop; + ID2D1LinearGradientBrush *brush; + ID2D1LinearGradientBrush *opacity; + ID2D1Layer *layer; + D2D1_LAYER_PARAMETERS layerparams; + D2D1_ELLIPSE mparam; + D2D1_COLOR_F mcolor; + ID2D1SolidColorBrush *markerBrush; + HRESULT hr; + + size = realGetSize(rt); + rect.left = 0; + rect.top = 0; + rect.right = size.width; + rect.bottom = size.height; + + drawGrid(rt, &rect); + + // first, draw a vertical gradient from the current hue at max S/V to black + // the source example draws it upside down; let's do so too just to be safe + hsv2RGB(c->h, 1.0, 1.0, &rTop, &gTop, &bTop); + stops[0].position = 0; + stops[0].color.r = 0.0; + stops[0].color.g = 0.0; + stops[0].color.b = 0.0; + stops[0].color.a = 1.0; + stops[1].position = 1; + stops[1].color.r = rTop; + stops[1].color.g = gTop; + stops[1].color.b = bTop; + stops[1].color.a = 1.0; + hr = rt->CreateGradientStopCollection(stops, 2, + D2D1_GAMMA_2_2, D2D1_EXTEND_MODE_CLAMP, + &collection); + if (hr != S_OK) + logHRESULT(L"error making gradient stop collection for first gradient in SV chooser", hr); + ZeroMemory(&lprop, sizeof (D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES)); + lprop.startPoint.x = size.width / 2; + lprop.startPoint.y = size.height; + lprop.endPoint.x = size.width / 2; + lprop.endPoint.y = 0; + // TODO decide what to do about the duplication of this + ZeroMemory(&bprop, sizeof (D2D1_BRUSH_PROPERTIES)); + bprop.opacity = c->a; // note this part; we also use it below for the layer + bprop.transform._11 = 1; + bprop.transform._22 = 1; + hr = rt->CreateLinearGradientBrush(&lprop, &bprop, + collection, &brush); + if (hr != S_OK) + logHRESULT(L"error making gradient brush for first gradient in SV chooser", hr); + rt->FillRectangle(&rect, brush); + brush->Release(); + collection->Release(); + + // second, create an opacity mask for the third step: a horizontal gradientthat goes from opaque to translucent + stops[0].position = 0; + stops[0].color.r = 0.0; + stops[0].color.g = 0.0; + stops[0].color.b = 0.0; + stops[0].color.a = 1.0; + stops[1].position = 1; + stops[1].color.r = 0.0; + stops[1].color.g = 0.0; + stops[1].color.b = 0.0; + stops[1].color.a = 0.0; + hr = rt->CreateGradientStopCollection(stops, 2, + D2D1_GAMMA_2_2, D2D1_EXTEND_MODE_CLAMP, + &collection); + if (hr != S_OK) + logHRESULT(L"error making gradient stop collection for opacity mask gradient in SV chooser", hr); + ZeroMemory(&lprop, sizeof (D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES)); + lprop.startPoint.x = 0; + lprop.startPoint.y = size.height / 2; + lprop.endPoint.x = size.width; + lprop.endPoint.y = size.height / 2; + ZeroMemory(&bprop, sizeof (D2D1_BRUSH_PROPERTIES)); + bprop.opacity = 1.0; + bprop.transform._11 = 1; + bprop.transform._22 = 1; + hr = rt->CreateLinearGradientBrush(&lprop, &bprop, + collection, &opacity); + if (hr != S_OK) + logHRESULT(L"error making gradient brush for opacity mask gradient in SV chooser", hr); + collection->Release(); + + // finally, make a vertical gradient from white at the top to black at the bottom (right side up this time) and with the previous opacity mask + stops[0].position = 0; + stops[0].color.r = 1.0; + stops[0].color.g = 1.0; + stops[0].color.b = 1.0; + stops[0].color.a = 1.0; + stops[1].position = 1; + stops[1].color.r = 0.0; + stops[1].color.g = 0.0; + stops[1].color.b = 0.0; + stops[1].color.a = 1.0; + hr = rt->CreateGradientStopCollection(stops, 2, + D2D1_GAMMA_2_2, D2D1_EXTEND_MODE_CLAMP, + &collection); + if (hr != S_OK) + logHRESULT(L"error making gradient stop collection for second gradient in SV chooser", hr); + ZeroMemory(&lprop, sizeof (D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES)); + lprop.startPoint.x = size.width / 2; + lprop.startPoint.y = 0; + lprop.endPoint.x = size.width / 2; + lprop.endPoint.y = size.height; + ZeroMemory(&bprop, sizeof (D2D1_BRUSH_PROPERTIES)); + bprop.opacity = 1.0; + bprop.transform._11 = 1; + bprop.transform._22 = 1; + hr = rt->CreateLinearGradientBrush(&lprop, &bprop, + collection, &brush); + if (hr != S_OK) + logHRESULT(L"error making gradient brush for second gradient in SV chooser", hr); + // oh but wait we can't use FillRectangle() with an opacity mask + // and we can't use FillGeometry() with both an opacity mask and a non-bitmap + // layers it is! + hr = rt->CreateLayer(&size, &layer); + if (hr != S_OK) + logHRESULT(L"error making layer for second gradient in SV chooser", hr); + ZeroMemory(&layerparams, sizeof (D2D1_LAYER_PARAMETERS)); + layerparams.contentBounds = rect; + // TODO make sure these are right + layerparams.geometricMask = NULL; + layerparams.maskAntialiasMode = D2D1_ANTIALIAS_MODE_PER_PRIMITIVE; + layerparams.maskTransform._11 = 1; + layerparams.maskTransform._22 = 1; + layerparams.opacity = c->a; // here's the other use of c->a to note + layerparams.opacityBrush = opacity; + layerparams.layerOptions = D2D1_LAYER_OPTIONS_NONE; + rt->PushLayer(&layerparams, layer); + rt->FillRectangle(&rect, brush); + rt->PopLayer(); + layer->Release(); + brush->Release(); + collection->Release(); + opacity->Release(); + + // and now we just draw the marker + ZeroMemory(&mparam, sizeof (D2D1_ELLIPSE)); + mparam.point.x = c->s * size.width; + mparam.point.y = (1 - c->v) * size.height; + mparam.radiusX = 7; + mparam.radiusY = 7; + // TODO make the color contrast? + mcolor.r = 1.0; + mcolor.g = 1.0; + mcolor.b = 1.0; + mcolor.a = 1.0; + bprop.opacity = 1.0; // the marker should always be opaque + hr = rt->CreateSolidColorBrush(&mcolor, &bprop, &markerBrush); + if (hr != S_OK) + logHRESULT(L"error creating brush for SV chooser marker", hr); + rt->DrawEllipse(&mparam, markerBrush, 2, NULL); + markerBrush->Release(); +} + +static LRESULT CALLBACK svChooserSubProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) +{ + ID2D1RenderTarget *rt; + struct colorDialog *c; + D2D1_POINT_2F *pos; + D2D1_SIZE_F *size; + + c = (struct colorDialog *) dwRefData; + switch (uMsg) { + case msgD2DScratchPaint: + rt = (ID2D1RenderTarget *) lParam; + drawSVChooser(c, rt); + return 0; + case msgD2DScratchLButtonDown: + pos = (D2D1_POINT_2F *) wParam; + size = (D2D1_SIZE_F *) lParam; + c->s = pos->x / size->width; + c->v = 1 - (pos->y / size->height); + updateDialog(c, NULL); + return 0; + case WM_NCDESTROY: + if (RemoveWindowSubclass(hwnd, svChooserSubProc, uIdSubclass) == FALSE) + logLastError(L"error removing color dialog SV chooser subclass"); + break; + } + return DefSubclassProc(hwnd, uMsg, wParam, lParam); +} + +static void drawArrow(ID2D1RenderTarget *rt, D2D1_POINT_2F center, double hypot) +{ + double leg; + D2D1_RECT_F rect; + D2D1_MATRIX_3X2_F oldtf, rotate; + D2D1_COLOR_F color; + D2D1_BRUSH_PROPERTIES bprop; + ID2D1SolidColorBrush *brush; + HRESULT hr; + + // to avoid needing a geometry, this will just be a rotated square + // compute the length of each side; the diagonal of the square is 2 * offset to gradient + // a^2 + a^2 = c^2 -> 2a^2 = c^2 + // a = sqrt(c^2/2) + hypot *= hypot; + hypot /= 2; + leg = sqrt(hypot); + rect.left = center.x - leg; + rect.top = center.y - leg; + rect.right = center.x + leg; + rect.bottom = center.y + leg; + + // now we need to rotate the render target 45° (either way works) about the center point + rt->GetTransform(&oldtf); + rotate = oldtf * D2D1::Matrix3x2F::Rotation(45, center); + rt->SetTransform(&rotate); + + // and draw + color.r = 0.0; + color.g = 0.0; + color.b = 0.0; + color.a = 1.0; + ZeroMemory(&bprop, sizeof (D2D1_BRUSH_PROPERTIES)); + bprop.opacity = 1.0; + bprop.transform._11 = 1; + bprop.transform._22 = 1; + hr = rt->CreateSolidColorBrush(&color, &bprop, &brush); + if (hr != S_OK) + logHRESULT(L"error creating brush for arrow", hr); + rt->FillRectangle(&rect, brush); + brush->Release(); + + // clean up + rt->SetTransform(&oldtf); +} + +// the gradient stuff also comes from http://blogs.msdn.com/b/wpfsdk/archive/2006/10/26/uncommon-dialogs--font-chooser-and-color-picker-dialogs.aspx +#define nStops (30) +#define degPerStop (360 / nStops) +#define stopIncr (1.0 / ((double) nStops)) + +static void drawHSlider(struct colorDialog *c, ID2D1RenderTarget *rt) +{ + D2D1_SIZE_F size; + D2D1_RECT_F rect; + D2D1_GRADIENT_STOP stops[nStops]; + double r, g, b; + int i; + double h; + ID2D1GradientStopCollection *collection; + D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES lprop; + D2D1_BRUSH_PROPERTIES bprop; + ID2D1LinearGradientBrush *brush; + double hypot; + D2D1_POINT_2F center; + HRESULT hr; + + size = realGetSize(rt); + rect.left = size.width / 6; // leftmost sixth for arrow + rect.top = 0; + rect.right = size.width; + rect.bottom = size.height; + + for (i = 0; i < nStops; i++) { + h = ((double) (i * degPerStop)) / 360.0; + if (i == (nStops - 1)) + h = 0; + hsv2RGB(h, 1.0, 1.0, &r, &g, &b); + stops[i].position = ((double) i) * stopIncr; + stops[i].color.r = r; + stops[i].color.g = g; + stops[i].color.b = b; + stops[i].color.a = 1.0; + } + // and pin the last one + stops[i - 1].position = 1.0; + + hr = rt->CreateGradientStopCollection(stops, nStops, + // note that in this case this gamma is explicitly specified by the original + D2D1_GAMMA_2_2, D2D1_EXTEND_MODE_CLAMP, + &collection); + if (hr != S_OK) + logHRESULT(L"error creating stop collection for H slider gradient", hr); + ZeroMemory(&lprop, sizeof (D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES)); + lprop.startPoint.x = (rect.right - rect.left) / 2; + lprop.startPoint.y = 0; + lprop.endPoint.x = (rect.right - rect.left) / 2; + lprop.endPoint.y = size.height; + ZeroMemory(&bprop, sizeof (D2D1_BRUSH_PROPERTIES)); + bprop.opacity = 1.0; + bprop.transform._11 = 1; + bprop.transform._22 = 1; + hr = rt->CreateLinearGradientBrush(&lprop, &bprop, + collection, &brush); + if (hr != S_OK) + logHRESULT(L"error creating gradient brush for H slider", hr); + rt->FillRectangle(&rect, brush); + brush->Release(); + collection->Release(); + + // now draw a black arrow + center.x = 0; + center.y = c->h * size.height; + hypot = rect.left; + drawArrow(rt, center, hypot); +} + +static LRESULT CALLBACK hSliderSubProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) +{ + ID2D1RenderTarget *rt; + struct colorDialog *c; + D2D1_POINT_2F *pos; + D2D1_SIZE_F *size; + + c = (struct colorDialog *) dwRefData; + switch (uMsg) { + case msgD2DScratchPaint: + rt = (ID2D1RenderTarget *) lParam; + drawHSlider(c, rt); + return 0; + case msgD2DScratchLButtonDown: + pos = (D2D1_POINT_2F *) wParam; + size = (D2D1_SIZE_F *) lParam; + c->h = pos->y / size->height; + updateDialog(c, NULL); + return 0; + case WM_NCDESTROY: + if (RemoveWindowSubclass(hwnd, hSliderSubProc, uIdSubclass) == FALSE) + logLastError(L"error removing color dialog H slider subclass"); + break; + } + return DefSubclassProc(hwnd, uMsg, wParam, lParam); +} + +static void drawPreview(struct colorDialog *c, ID2D1RenderTarget *rt) +{ + D2D1_SIZE_F size; + D2D1_RECT_F rect; + double r, g, b; + D2D1_COLOR_F color; + D2D1_BRUSH_PROPERTIES bprop; + ID2D1SolidColorBrush *brush; + HRESULT hr; + + size = realGetSize(rt); + rect.left = 0; + rect.top = 0; + rect.right = size.width; + rect.bottom = size.height; + + drawGrid(rt, &rect); + + hsv2RGB(c->h, c->s, c->v, &r, &g, &b); + color.r = r; + color.g = g; + color.b = b; + color.a = c->a; + ZeroMemory(&bprop, sizeof (D2D1_BRUSH_PROPERTIES)); + bprop.opacity = 1.0; + bprop.transform._11 = 1; + bprop.transform._22 = 1; + hr = rt->CreateSolidColorBrush(&color, &bprop, &brush); + if (hr != S_OK) + logHRESULT(L"error creating brush for preview", hr); + rt->FillRectangle(&rect, brush); + brush->Release(); +} + +static LRESULT CALLBACK previewSubProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) +{ + ID2D1RenderTarget *rt; + struct colorDialog *c; + + c = (struct colorDialog *) dwRefData; + switch (uMsg) { + case msgD2DScratchPaint: + rt = (ID2D1RenderTarget *) lParam; + drawPreview(c, rt); + return 0; + case WM_NCDESTROY: + if (RemoveWindowSubclass(hwnd, previewSubProc, uIdSubclass) == FALSE) + logLastError(L"error removing color dialog previewer subclass"); + break; + } + return DefSubclassProc(hwnd, uMsg, wParam, lParam); +} + +// once again, this is based on the Microsoft sample above +static void drawOpacitySlider(struct colorDialog *c, ID2D1RenderTarget *rt) +{ + D2D1_SIZE_F size; + D2D1_RECT_F rect; + D2D1_GRADIENT_STOP stops[2]; + ID2D1GradientStopCollection *collection; + D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES lprop; + D2D1_BRUSH_PROPERTIES bprop; + ID2D1LinearGradientBrush *brush; + double hypot; + D2D1_POINT_2F center; + HRESULT hr; + + size = realGetSize(rt); + rect.left = 0; + rect.top = 0; + rect.right = size.width; + rect.bottom = size.height * (5.0 / 6.0); // bottommost sixth for arrow + + drawGrid(rt, &rect); + + stops[0].position = 0.0; + stops[0].color.r = 0.0; + stops[0].color.g = 0.0; + stops[0].color.b = 0.0; + stops[0].color.a = 1.0; + stops[1].position = 1.0; + stops[1].color.r = 1.0; // this is the XAML color Transparent, as in the source + stops[1].color.g = 1.0; + stops[1].color.b = 1.0; + stops[1].color.a = 0.0; + hr = rt->CreateGradientStopCollection(stops, 2, + // note that in this case this gamma is explicitly specified by the original + D2D1_GAMMA_2_2, D2D1_EXTEND_MODE_CLAMP, + &collection); + if (hr != S_OK) + logHRESULT(L"error creating stop collection for opacity slider gradient", hr); + ZeroMemory(&lprop, sizeof (D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES)); + lprop.startPoint.x = 0; + lprop.startPoint.y = (rect.bottom - rect.top) / 2; + lprop.endPoint.x = size.width; + lprop.endPoint.y = (rect.bottom - rect.top) / 2; + ZeroMemory(&bprop, sizeof (D2D1_BRUSH_PROPERTIES)); + bprop.opacity = 1.0; + bprop.transform._11 = 1; + bprop.transform._22 = 1; + hr = rt->CreateLinearGradientBrush(&lprop, &bprop, + collection, &brush); + if (hr != S_OK) + logHRESULT(L"error creating gradient brush for opacity slider", hr); + rt->FillRectangle(&rect, brush); + brush->Release(); + collection->Release(); + + // now draw a black arrow + center.x = (1 - c->a) * size.width; + center.y = size.height; + hypot = size.height - rect.bottom; + drawArrow(rt, center, hypot); +} + +static LRESULT CALLBACK opacitySliderSubProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) +{ + ID2D1RenderTarget *rt; + struct colorDialog *c; + D2D1_POINT_2F *pos; + D2D1_SIZE_F *size; + + c = (struct colorDialog *) dwRefData; + switch (uMsg) { + case msgD2DScratchPaint: + rt = (ID2D1RenderTarget *) lParam; + drawOpacitySlider(c, rt); + return 0; + case msgD2DScratchLButtonDown: + pos = (D2D1_POINT_2F *) wParam; + size = (D2D1_SIZE_F *) lParam; + c->a = 1 - (pos->x / size->width); + updateDialog(c, NULL); + return 0; + case WM_NCDESTROY: + if (RemoveWindowSubclass(hwnd, opacitySliderSubProc, uIdSubclass) == FALSE) + logLastError(L"error removing color dialog opacity slider subclass"); + break; + } + return DefSubclassProc(hwnd, uMsg, wParam, lParam); +} + +// TODO extract into d2dscratch.cpp, use in font dialog +HWND replaceWithD2DScratch(HWND parent, int id, SUBCLASSPROC subproc, void *data) +{ + HWND replace; + RECT r; + + replace = getDlgItem(parent, id); + uiWindowsEnsureGetWindowRect(replace, &r); + mapWindowRect(NULL, parent, &r); + uiWindowsEnsureDestroyWindow(replace); + return newD2DScratch(parent, &r, (HMENU) id, subproc, (DWORD_PTR) data); + // TODO preserve Z-order +} + +// a few issues: +// - some controls are positioned wrong; see http://stackoverflow.com/questions/37263267/why-are-some-of-my-controls-positioned-slightly-off-in-a-dialog-template-in-a-re +// - labels are too low; need to adjust them by the font's internal leading +// fixupControlPositions() and the following helper routines fix that for us + +static LONG offsetTo(HWND a, HWND b) +{ + RECT ra, rb; + + uiWindowsEnsureGetWindowRect(a, &ra); + uiWindowsEnsureGetWindowRect(b, &rb); + return rb.top - ra.bottom; +} + +static void moveWindowsUp(struct colorDialog *c, LONG by, ...) +{ + va_list ap; + HWND cur; + RECT r; + + va_start(ap, by); + for (;;) { + cur = va_arg(ap, HWND); + if (cur == NULL) + break; + uiWindowsEnsureGetWindowRect(cur, &r); + mapWindowRect(NULL, c->hwnd, &r); + r.top -= by; + r.bottom -= by; + // TODO this isn't technically during a resize + uiWindowsEnsureMoveWindowDuringResize(cur, + r.left, r.top, + r.right - r.left, r.bottom - r.top); + } + va_end(ap); +} + +static void fixupControlPositions(struct colorDialog *c) +{ + HWND labelH; + HWND labelS; + HWND labelV; + HWND labelR; + HWND labelG; + HWND labelB; + HWND labelA; + HWND labelHex; + LONG offset; + uiWindowsSizing sizing; + + labelH = getDlgItem(c->hwnd, rcHLabel); + labelS = getDlgItem(c->hwnd, rcSLabel); + labelV = getDlgItem(c->hwnd, rcVLabel); + labelR = getDlgItem(c->hwnd, rcRLabel); + labelG = getDlgItem(c->hwnd, rcGLabel); + labelB = getDlgItem(c->hwnd, rcBLabel); + labelA = getDlgItem(c->hwnd, rcALabel); + labelHex = getDlgItem(c->hwnd, rcHexLabel); + + offset = offsetTo(c->editH, c->editS); + moveWindowsUp(c, offset, + labelS, c->editS, + labelG, c->editGDouble, c->editGInt, + NULL); + offset = offsetTo(c->editS, c->editV); + moveWindowsUp(c, offset, + labelV, c->editV, + labelB, c->editBDouble, c->editBInt, + NULL); + offset = offsetTo(c->editBDouble, c->editADouble); + moveWindowsUp(c, offset, + labelA, c->editADouble, c->editAInt, + NULL); + + getSizing(c->hwnd, &sizing, (HFONT) SendMessageW(labelH, WM_GETFONT, 0, 0)); + offset = sizing.InternalLeading; + moveWindowsUp(c, offset, + labelH, labelS, labelV, + labelR, labelG, labelB, labelA, + labelHex, + NULL); +} + +static struct colorDialog *beginColorDialog(HWND hwnd, LPARAM lParam) +{ + struct colorDialog *c; + + c = uiNew(struct colorDialog); + c->hwnd = hwnd; + c->out = (struct colorDialogRGBA *) lParam; + // load initial values now + rgb2HSV(c->out->r, c->out->g, c->out->b, &(c->h), &(c->s), &(c->v)); + c->a = c->out->a; + + // TODO set up d2dscratches + + // TODO prefix all these with rcColor instead of just rc + c->editH = getDlgItem(c->hwnd, rcH); + c->editS = getDlgItem(c->hwnd, rcS); + c->editV = getDlgItem(c->hwnd, rcV); + c->editRDouble = getDlgItem(c->hwnd, rcRDouble); + c->editRInt = getDlgItem(c->hwnd, rcRInt); + c->editGDouble = getDlgItem(c->hwnd, rcGDouble); + c->editGInt = getDlgItem(c->hwnd, rcGInt); + c->editBDouble = getDlgItem(c->hwnd, rcBDouble); + c->editBInt = getDlgItem(c->hwnd, rcBInt); + c->editADouble = getDlgItem(c->hwnd, rcADouble); + c->editAInt = getDlgItem(c->hwnd, rcAInt); + c->editHex = getDlgItem(c->hwnd, rcHex); + + c->svChooser = replaceWithD2DScratch(c->hwnd, rcColorSVChooser, svChooserSubProc, c); + c->hSlider = replaceWithD2DScratch(c->hwnd, rcColorHSlider, hSliderSubProc, c); + c->preview = replaceWithD2DScratch(c->hwnd, rcPreview, previewSubProc, c); + c->opacitySlider = replaceWithD2DScratch(c->hwnd, rcOpacitySlider, opacitySliderSubProc, c); + + fixupControlPositions(c); + + // and get the ball rolling + updateDialog(c, NULL); + return c; +} + +static void endColorDialog(struct colorDialog *c, INT_PTR code) +{ + if (EndDialog(c->hwnd, code) == 0) + logLastError(L"error ending color dialog"); + uiFree(c); +} + +// TODO make this void on the font dialog too +static void tryFinishDialog(struct colorDialog *c, WPARAM wParam) +{ + // cancelling + if (LOWORD(wParam) != IDOK) { + endColorDialog(c, 1); + return; + } + + // OK + hsv2RGB(c->h, c->s, c->v, &(c->out->r), &(c->out->g), &(c->out->b)); + c->out->a = c->a; + endColorDialog(c, 2); +} + +static double editDouble(HWND hwnd) +{ + WCHAR *s; + double d; + + s = windowText(hwnd); + d = _wtof(s); + uiFree(s); + return d; +} + +static void hChanged(struct colorDialog *c) +{ + double h; + + h = editDouble(c->editH); + if (h < 0 || h >= 1.0) // note the >= + return; + c->h = h; + updateDialog(c, c->editH); +} + +static void sChanged(struct colorDialog *c) +{ + double s; + + s = editDouble(c->editS); + if (s < 0 || s > 1) + return; + c->s = s; + updateDialog(c, c->editS); +} + +static void vChanged(struct colorDialog *c) +{ + double v; + + v = editDouble(c->editV); + if (v < 0 || v > 1) + return; + c->v = v; + updateDialog(c, c->editV); +} + +static void rDoubleChanged(struct colorDialog *c) +{ + double r, g, b; + + hsv2RGB(c->h, c->s, c->v, &r, &g, &b); + r = editDouble(c->editRDouble); + if (r < 0 || r > 1) + return; + rgb2HSV(r, g, b, &(c->h), &(c->s), &(c->v)); + updateDialog(c, c->editRDouble); +} + +static void gDoubleChanged(struct colorDialog *c) +{ + double r, g, b; + + hsv2RGB(c->h, c->s, c->v, &r, &g, &b); + g = editDouble(c->editGDouble); + if (g < 0 || g > 1) + return; + rgb2HSV(r, g, b, &(c->h), &(c->s), &(c->v)); + updateDialog(c, c->editGDouble); +} + +static void bDoubleChanged(struct colorDialog *c) +{ + double r, g, b; + + hsv2RGB(c->h, c->s, c->v, &r, &g, &b); + b = editDouble(c->editBDouble); + if (b < 0 || b > 1) + return; + rgb2HSV(r, g, b, &(c->h), &(c->s), &(c->v)); + updateDialog(c, c->editBDouble); +} + +static void aDoubleChanged(struct colorDialog *c) +{ + double a; + + a = editDouble(c->editADouble); + if (a < 0 || a > 1) + return; + c->a = a; + updateDialog(c, c->editADouble); +} + +static int editInt(HWND hwnd) +{ + WCHAR *s; + int i; + + s = windowText(hwnd); + i = _wtoi(s); + uiFree(s); + return i; +} + +static void rIntChanged(struct colorDialog *c) +{ + double r, g, b; + int i; + + hsv2RGB(c->h, c->s, c->v, &r, &g, &b); + i = editInt(c->editRInt); + if (i < 0 || i > 255) + return; + r = ((double) i) / 255.0; + rgb2HSV(r, g, b, &(c->h), &(c->s), &(c->v)); + updateDialog(c, c->editRInt); +} + +static void gIntChanged(struct colorDialog *c) +{ + double r, g, b; + int i; + + hsv2RGB(c->h, c->s, c->v, &r, &g, &b); + i = editInt(c->editGInt); + if (i < 0 || i > 255) + return; + g = ((double) i) / 255.0; + rgb2HSV(r, g, b, &(c->h), &(c->s), &(c->v)); + updateDialog(c, c->editGInt); +} + +static void bIntChanged(struct colorDialog *c) +{ + double r, g, b; + int i; + + hsv2RGB(c->h, c->s, c->v, &r, &g, &b); + i = editInt(c->editBInt); + if (i < 0 || i > 255) + return; + b = ((double) i) / 255.0; + rgb2HSV(r, g, b, &(c->h), &(c->s), &(c->v)); + updateDialog(c, c->editBInt); +} + +static void aIntChanged(struct colorDialog *c) +{ + int a; + + a = editInt(c->editAInt); + if (a < 0 || a > 255) + return; + c->a = ((double) a) / 255; + updateDialog(c, c->editAInt); +} + +static void hexChanged(struct colorDialog *c) +{ + WCHAR *buf; + double r, g, b, a; + BOOL is; + + buf = windowText(c->editHex); + is = hex2RGBA(buf, &r, &g, &b, &a); + uiFree(buf); + if (!is) + return; + rgb2HSV(r, g, b, &(c->h), &(c->s), &(c->v)); + c->a = a; + updateDialog(c, c->editHex); +} + +// TODO change fontdialog to use this +// note that if we make this const, we get lots of weird compiler errors +static std::map<int, void (*)(struct colorDialog *)> changed = { + { rcH, hChanged }, + { rcS, sChanged }, + { rcV, vChanged }, + { rcRDouble, rDoubleChanged }, + { rcGDouble, gDoubleChanged }, + { rcBDouble, bDoubleChanged }, + { rcADouble, aDoubleChanged }, + { rcRInt, rIntChanged }, + { rcGInt, gIntChanged }, + { rcBInt, bIntChanged }, + { rcAInt, aIntChanged }, + { rcHex, hexChanged }, +}; + +static INT_PTR CALLBACK colorDialogDlgProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + struct colorDialog *c; + + c = (struct colorDialog *) GetWindowLongPtrW(hwnd, DWLP_USER); + if (c == NULL) { + if (uMsg == WM_INITDIALOG) { + c = beginColorDialog(hwnd, lParam); + SetWindowLongPtrW(hwnd, DWLP_USER, (LONG_PTR) c); + return TRUE; + } + return FALSE; + } + + switch (uMsg) { + case WM_COMMAND: + SetWindowLongPtrW(c->hwnd, DWLP_MSGRESULT, 0); // just in case + switch (LOWORD(wParam)) { + case IDOK: + case IDCANCEL: + if (HIWORD(wParam) != BN_CLICKED) + return FALSE; + tryFinishDialog(c, wParam); + return TRUE; + case rcH: + case rcS: + case rcV: + case rcRDouble: + case rcGDouble: + case rcBDouble: + case rcADouble: + case rcRInt: + case rcGInt: + case rcBInt: + case rcAInt: + case rcHex: + if (HIWORD(wParam) != EN_CHANGE) + return FALSE; + if (c->updating) // prevent infinite recursion during an update + return FALSE; + (*(changed[LOWORD(wParam)]))(c); + return TRUE; + } + return FALSE; + } + return FALSE; +} + +BOOL showColorDialog(HWND parent, struct colorDialogRGBA *c) +{ + switch (DialogBoxParamW(hInstance, MAKEINTRESOURCE(rcColorDialog), parent, colorDialogDlgProc, (LPARAM) c)) { + case 1: // cancel + return FALSE; + case 2: // ok + // make the compiler happy by putting the return after the switch + break; + default: + logLastError(L"error running color dialog"); + } + return TRUE; +} diff --git a/src/libui_sdl/libui/windows/combobox.cpp b/src/libui_sdl/libui/windows/combobox.cpp new file mode 100644 index 0000000..87c999e --- /dev/null +++ b/src/libui_sdl/libui/windows/combobox.cpp @@ -0,0 +1,110 @@ +// 20 may 2015 +#include "uipriv_windows.hpp" + +// we as Common Controls 6 users don't need to worry about the height of comboboxes; see http://blogs.msdn.com/b/oldnewthing/archive/2006/03/10/548537.aspx + +struct uiCombobox { + uiWindowsControl c; + HWND hwnd; + void (*onSelected)(uiCombobox *, void *); + void *onSelectedData; +}; + +static BOOL onWM_COMMAND(uiControl *cc, HWND hwnd, WORD code, LRESULT *lResult) +{ + uiCombobox *c = uiCombobox(cc); + + if (code != CBN_SELCHANGE) + return FALSE; + (*(c->onSelected))(c, c->onSelectedData); + *lResult = 0; + return TRUE; +} + +void uiComboboxDestroy(uiControl *cc) +{ + uiCombobox *c = uiCombobox(cc); + + uiWindowsUnregisterWM_COMMANDHandler(c->hwnd); + uiWindowsEnsureDestroyWindow(c->hwnd); + uiFreeControl(uiControl(c)); +} + +uiWindowsControlAllDefaultsExceptDestroy(uiCombobox) + +// from http://msdn.microsoft.com/en-us/library/windows/desktop/dn742486.aspx#sizingandspacing +#define comboboxWidth 107 /* this is actually the shorter progress bar width, but Microsoft only indicates as wide as necessary; LONGTERM */ +#define comboboxHeight 14 /* LONGTERM: is this too high? */ + +static void uiComboboxMinimumSize(uiWindowsControl *cc, int *width, int *height) +{ + uiCombobox *c = uiCombobox(cc); + uiWindowsSizing sizing; + int x, y; + + x = comboboxWidth; + y = comboboxHeight; + uiWindowsGetSizing(c->hwnd, &sizing); + uiWindowsSizingDlgUnitsToPixels(&sizing, &x, &y); + *width = x; + *height = y; +} + +static void defaultOnSelected(uiCombobox *c, void *data) +{ + // do nothing +} + +void uiComboboxAppend(uiCombobox *c, const char *text) +{ + WCHAR *wtext; + LRESULT res; + + wtext = toUTF16(text); + res = SendMessageW(c->hwnd, CB_ADDSTRING, 0, (LPARAM) wtext); + if (res == (LRESULT) CB_ERR) + logLastError(L"error appending item to uiCombobox"); + else if (res == (LRESULT) CB_ERRSPACE) + logLastError(L"memory exhausted appending item to uiCombobox"); + uiFree(wtext); +} + +int uiComboboxSelected(uiCombobox *c) +{ + LRESULT n; + + n = SendMessage(c->hwnd, CB_GETCURSEL, 0, 0); + if (n == (LRESULT) CB_ERR) + return -1; + return n; +} + +void uiComboboxSetSelected(uiCombobox *c, int n) +{ + // TODO error check + SendMessageW(c->hwnd, CB_SETCURSEL, (WPARAM) n, 0); +} + +void uiComboboxOnSelected(uiCombobox *c, void (*f)(uiCombobox *c, void *data), void *data) +{ + c->onSelected = f; + c->onSelectedData = data; +} + +uiCombobox *uiNewCombobox(void) +{ + uiCombobox *c; + + uiWindowsNewControl(uiCombobox, c); + + c->hwnd = uiWindowsEnsureCreateControlHWND(WS_EX_CLIENTEDGE, + L"combobox", L"", + CBS_DROPDOWNLIST | WS_TABSTOP, + hInstance, NULL, + TRUE); + + uiWindowsRegisterWM_COMMANDHandler(c->hwnd, onWM_COMMAND, uiControl(c)); + uiComboboxOnSelected(c, defaultOnSelected, NULL); + + return c; +} diff --git a/src/libui_sdl/libui/windows/compilerver.hpp b/src/libui_sdl/libui/windows/compilerver.hpp new file mode 100644 index 0000000..6c9e6b8 --- /dev/null +++ b/src/libui_sdl/libui/windows/compilerver.hpp @@ -0,0 +1,13 @@ +// 9 june 2015 + +// Visual Studio (Microsoft's compilers) +// VS2013 is needed for va_copy(). +#ifdef _MSC_VER +#if _MSC_VER < 1800 +#error Visual Studio 2013 or higher is required to build libui. +#endif +#endif + +// LONGTERM MinGW + +// other compilers can be added here as necessary diff --git a/src/libui_sdl/libui/windows/container.cpp b/src/libui_sdl/libui/windows/container.cpp new file mode 100644 index 0000000..9ec1e28 --- /dev/null +++ b/src/libui_sdl/libui/windows/container.cpp @@ -0,0 +1,110 @@ +// 26 april 2015 +#include "uipriv_windows.hpp" + +// Code for the HWND of the following uiControls: +// - uiBox +// - uiRadioButtons +// - uiSpinbox +// - uiTab +// - uiForm +// - uiGrid + +struct containerInit { + uiWindowsControl *c; + void (*onResize)(uiWindowsControl *); +}; + +static LRESULT CALLBACK containerWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + RECT r; + HDC dc; + PAINTSTRUCT ps; + CREATESTRUCTW *cs = (CREATESTRUCTW *) lParam; + WINDOWPOS *wp = (WINDOWPOS *) lParam; + MINMAXINFO *mmi = (MINMAXINFO *) lParam; + struct containerInit *init; + uiWindowsControl *c; + void (*onResize)(uiWindowsControl *); + int minwid, minht; + LRESULT lResult; + + if (handleParentMessages(hwnd, uMsg, wParam, lParam, &lResult) != FALSE) + return lResult; + switch (uMsg) { + case WM_CREATE: + init = (struct containerInit *) (cs->lpCreateParams); + SetWindowLongPtrW(hwnd, GWLP_USERDATA, (LONG_PTR) (init->onResize)); + SetWindowLongPtrW(hwnd, 0, (LONG_PTR) (init->c)); + break; // defer to DefWindowProc() + case WM_WINDOWPOSCHANGED: + if ((wp->flags & SWP_NOSIZE) != 0) + break; // defer to DefWindowProc(); + onResize = (void (*)(uiWindowsControl *)) GetWindowLongPtrW(hwnd, GWLP_USERDATA); + c = (uiWindowsControl *) GetWindowLongPtrW(hwnd, 0); + (*(onResize))(c); + return 0; + case WM_GETMINMAXINFO: + lResult = DefWindowProcW(hwnd, uMsg, wParam, lParam); + c = (uiWindowsControl *) GetWindowLongPtrW(hwnd, 0); + uiWindowsControlMinimumSize(c, &minwid, &minht); + mmi->ptMinTrackSize.x = minwid; + mmi->ptMinTrackSize.y = minht; + return lResult; + case WM_PAINT: + dc = BeginPaint(hwnd, &ps); + if (dc == NULL) { + logLastError(L"error beginning container paint"); + // bail out; hope DefWindowProc() catches us + break; + } + r = ps.rcPaint; + paintContainerBackground(hwnd, dc, &r); + EndPaint(hwnd, &ps); + return 0; + // tab controls use this to draw the background of the tab area + case WM_PRINTCLIENT: + uiWindowsEnsureGetClientRect(hwnd, &r); + paintContainerBackground(hwnd, (HDC) wParam, &r); + return 0; + case WM_ERASEBKGND: + // avoid some flicker + // we draw the whole update area anyway + return 1; + } + return DefWindowProcW(hwnd, uMsg, wParam, lParam); +} + +ATOM initContainer(HICON hDefaultIcon, HCURSOR hDefaultCursor) +{ + WNDCLASSW wc; + + ZeroMemory(&wc, sizeof (WNDCLASSW)); + wc.lpszClassName = containerClass; + wc.lpfnWndProc = containerWndProc; + wc.hInstance = hInstance; + wc.hIcon = hDefaultIcon; + wc.hCursor = hDefaultCursor; + wc.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1); + wc.cbWndExtra = sizeof (void *); + return RegisterClassW(&wc); +} + +void uninitContainer(void) +{ + if (UnregisterClassW(containerClass, hInstance) == 0) + logLastError(L"error unregistering container window class"); +} + +HWND uiWindowsMakeContainer(uiWindowsControl *c, void (*onResize)(uiWindowsControl *)) +{ + struct containerInit init; + + // TODO onResize cannot be NULL + init.c = c; + init.onResize = onResize; + return uiWindowsEnsureCreateControlHWND(WS_EX_CONTROLPARENT, + containerClass, L"", + 0, + hInstance, (LPVOID) (&init), + FALSE); +} diff --git a/src/libui_sdl/libui/windows/control.cpp b/src/libui_sdl/libui/windows/control.cpp new file mode 100644 index 0000000..ce953cf --- /dev/null +++ b/src/libui_sdl/libui/windows/control.cpp @@ -0,0 +1,121 @@ +// 16 august 2015 +#include "uipriv_windows.hpp" + +void uiWindowsControlSyncEnableState(uiWindowsControl *c, int enabled) +{ + (*(c->SyncEnableState))(c, enabled); +} + +void uiWindowsControlSetParentHWND(uiWindowsControl *c, HWND parent) +{ + (*(c->SetParentHWND))(c, parent); +} + +void uiWindowsControlMinimumSize(uiWindowsControl *c, int *width, int *height) +{ + (*(c->MinimumSize))(c, width, height); +} + +void uiWindowsControlMinimumSizeChanged(uiWindowsControl *c) +{ + (*(c->MinimumSizeChanged))(c); +} + +// TODO get rid of this +void uiWindowsControlLayoutRect(uiWindowsControl *c, RECT *r) +{ + (*(c->LayoutRect))(c, r); +} + +void uiWindowsControlAssignControlIDZOrder(uiWindowsControl *c, LONG_PTR *controlID, HWND *insertAfter) +{ + (*(c->AssignControlIDZOrder))(c, controlID, insertAfter); +} + +void uiWindowsControlChildVisibilityChanged(uiWindowsControl *c) +{ + (*(c->ChildVisibilityChanged))(c); +} + +HWND uiWindowsEnsureCreateControlHWND(DWORD dwExStyle, LPCWSTR lpClassName, LPCWSTR lpWindowName, DWORD dwStyle, HINSTANCE hInstance, LPVOID lpParam, BOOL useStandardControlFont) +{ + HWND hwnd; + + // don't let using the arrow keys in a uiRadioButtons leave the radio buttons + if ((dwStyle & WS_TABSTOP) != 0) + dwStyle |= WS_GROUP; + hwnd = CreateWindowExW(dwExStyle, + lpClassName, lpWindowName, + dwStyle | WS_CHILD | WS_VISIBLE, + 0, 0, + // use a nonzero initial size just in case some control breaks with a zero initial size + 100, 100, + utilWindow, NULL, hInstance, lpParam); + if (hwnd == NULL) { + logLastError(L"error creating window"); + // TODO return a decoy window + } + if (useStandardControlFont) + SendMessageW(hwnd, WM_SETFONT, (WPARAM) hMessageFont, (LPARAM) TRUE); + return hwnd; +} + +// choose a value distinct from uiWindowSignature +#define uiWindowsControlSignature 0x4D53576E + +uiWindowsControl *uiWindowsAllocControl(size_t n, uint32_t typesig, const char *typenamestr) +{ + return uiWindowsControl(uiAllocControl(n, uiWindowsControlSignature, typesig, typenamestr)); +} + +BOOL uiWindowsShouldStopSyncEnableState(uiWindowsControl *c, BOOL enabled) +{ + int ce; + + ce = uiControlEnabled(uiControl(c)); + // only stop if we're going from disabled back to enabled; don't stop under any other condition + // (if we stop when going from enabled to disabled then enabled children of a disabled control won't get disabled at the OS level) + if (!ce && enabled) + return TRUE; + return FALSE; +} + +void uiWindowsControlAssignSoleControlIDZOrder(uiWindowsControl *c) +{ + LONG_PTR controlID; + HWND insertAfter; + + controlID = 100; + insertAfter = NULL; + uiWindowsControlAssignControlIDZOrder(c, &controlID, &insertAfter); +} + +BOOL uiWindowsControlTooSmall(uiWindowsControl *c) +{ + RECT r; + int width, height; + + uiWindowsControlLayoutRect(c, &r); + uiWindowsControlMinimumSize(c, &width, &height); + if ((r.right - r.left) < width) + return TRUE; + if ((r.bottom - r.top) < height) + return TRUE; + return FALSE; +} + +void uiWindowsControlContinueMinimumSizeChanged(uiWindowsControl *c) +{ + uiControl *parent; + + parent = uiControlParent(uiControl(c)); + if (parent != NULL) + uiWindowsControlMinimumSizeChanged(uiWindowsControl(parent)); +} + +// TODO rename this nad the OS X this and hugging ones to NotifyChild +void uiWindowsControlNotifyVisibilityChanged(uiWindowsControl *c) +{ + // TODO we really need to figure this out; the duplication is a mess + uiWindowsControlContinueMinimumSizeChanged(c); +} diff --git a/src/libui_sdl/libui/windows/d2dscratch.cpp b/src/libui_sdl/libui/windows/d2dscratch.cpp new file mode 100644 index 0000000..6dc2ba5 --- /dev/null +++ b/src/libui_sdl/libui/windows/d2dscratch.cpp @@ -0,0 +1,166 @@ +// 17 april 2016 +#include "uipriv_windows.hpp" + +// The Direct2D scratch window is a utility for libui internal use to do quick things with Direct2D. +// To use, call newD2DScratch() passing in a subclass procedure. This subclass procedure should handle the msgD2DScratchPaint message, which has the following usage: +// - wParam - 0 +// - lParam - ID2D1RenderTarget * +// - lResult - 0 +// You can optionally also handle msgD2DScratchLButtonDown, which is sent when the left mouse button is either pressed for the first time or held while the mouse is moving. +// - wParam - position in DIPs, as D2D1_POINT_2F * +// - lParam - size of render target in DIPs, as D2D1_SIZE_F * +// - lResult - 0 +// Other messages can also be handled here. + +// TODO allow resize + +#define d2dScratchClass L"libui_d2dScratchClass" + +// TODO clip rect +static HRESULT d2dScratchDoPaint(HWND hwnd, ID2D1RenderTarget *rt) +{ + COLORREF bgcolorref; + D2D1_COLOR_F bgcolor; + + rt->BeginDraw(); + + // TODO only clear the clip area + // TODO clear with actual background brush + bgcolorref = GetSysColor(COLOR_BTNFACE); + bgcolor.r = ((float) GetRValue(bgcolorref)) / 255.0; + // due to utter apathy on Microsoft's part, GetGValue() does not work with MSVC's Run-Time Error Checks + // it has not worked since 2008 and they have *never* fixed it + // TODO now that -RTCc has just been deprecated entirely, should we switch back? + bgcolor.g = ((float) ((BYTE) ((bgcolorref & 0xFF00) >> 8))) / 255.0; + bgcolor.b = ((float) GetBValue(bgcolorref)) / 255.0; + bgcolor.a = 1.0; + rt->Clear(&bgcolor); + + SendMessageW(hwnd, msgD2DScratchPaint, 0, (LPARAM) rt); + + return rt->EndDraw(NULL, NULL); +} + +static void d2dScratchDoLButtonDown(HWND hwnd, ID2D1RenderTarget *rt, LPARAM lParam) +{ + double xpix, ypix; + FLOAT dpix, dpiy; + D2D1_POINT_2F pos; + D2D1_SIZE_F size; + + xpix = (double) GET_X_LPARAM(lParam); + ypix = (double) GET_Y_LPARAM(lParam); + // these are in pixels; we need points + // TODO separate the function from areautil.cpp? + rt->GetDpi(&dpix, &dpiy); + pos.x = (xpix * 96) / dpix; + pos.y = (ypix * 96) / dpiy; + + size = realGetSize(rt); + + SendMessageW(hwnd, msgD2DScratchLButtonDown, (WPARAM) (&pos), (LPARAM) (&size)); +} + +static LRESULT CALLBACK d2dScratchWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + LONG_PTR init; + ID2D1HwndRenderTarget *rt; + ID2D1DCRenderTarget *dcrt; + RECT client; + HRESULT hr; + + init = GetWindowLongPtrW(hwnd, 0); + if (!init) { + if (uMsg == WM_CREATE) + SetWindowLongPtrW(hwnd, 0, (LONG_PTR) TRUE); + return DefWindowProcW(hwnd, uMsg, wParam, lParam); + } + + rt = (ID2D1HwndRenderTarget *) GetWindowLongPtrW(hwnd, GWLP_USERDATA); + if (rt == NULL) { + rt = makeHWNDRenderTarget(hwnd); + SetWindowLongPtrW(hwnd, GWLP_USERDATA, (LONG_PTR) rt); + } + + switch (uMsg) { + case WM_DESTROY: + rt->Release(); + SetWindowLongPtrW(hwnd, 0, (LONG_PTR) FALSE); + break; + case WM_PAINT: + hr = d2dScratchDoPaint(hwnd, rt); + switch (hr) { + case S_OK: + if (ValidateRect(hwnd, NULL) == 0) + logLastError(L"error validating D2D scratch control rect"); + break; + case D2DERR_RECREATE_TARGET: + // DON'T validate the rect + // instead, simply drop the render target + // we'll get another WM_PAINT and make the render target again + // TODO would this require us to invalidate the entire client area? + rt->Release(); + SetWindowLongPtrW(hwnd, GWLP_USERDATA, (LONG_PTR) NULL); + break; + default: + logHRESULT(L"error drawing D2D scratch window", hr); + } + return 0; + case WM_PRINTCLIENT: + uiWindowsEnsureGetClientRect(hwnd, &client); + dcrt = makeHDCRenderTarget((HDC) wParam, &client); + hr = d2dScratchDoPaint(hwnd, dcrt); + if (hr != S_OK) + logHRESULT(L"error printing D2D scratch window client area", hr); + dcrt->Release(); + return 0; + case WM_LBUTTONDOWN: + d2dScratchDoLButtonDown(hwnd, rt, lParam); + return 0; + case WM_MOUSEMOVE: + // also send LButtonDowns when dragging + if ((wParam & MK_LBUTTON) != 0) + d2dScratchDoLButtonDown(hwnd, rt, lParam); + return 0; + } + return DefWindowProcW(hwnd, uMsg, wParam, lParam); +} + +ATOM registerD2DScratchClass(HICON hDefaultIcon, HCURSOR hDefaultCursor) +{ + WNDCLASSW wc; + + ZeroMemory(&wc, sizeof (WNDCLASSW)); + wc.lpszClassName = d2dScratchClass; + wc.lpfnWndProc = d2dScratchWndProc; + wc.hInstance = hInstance; + wc.hIcon = hDefaultIcon; + wc.hCursor = hDefaultCursor; + wc.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1); + wc.cbWndExtra = sizeof (LONG_PTR); // for the init status + return RegisterClassW(&wc); +} + +void unregisterD2DScratchClass(void) +{ + if (UnregisterClassW(d2dScratchClass, hInstance) == 0) + logLastError(L"error unregistering D2D scratch window class"); +} + +HWND newD2DScratch(HWND parent, RECT *rect, HMENU controlID, SUBCLASSPROC subclass, DWORD_PTR subclassData) +{ + HWND hwnd; + + hwnd = CreateWindowExW(0, + d2dScratchClass, L"", + WS_CHILD | WS_VISIBLE, + rect->left, rect->top, + rect->right - rect->left, rect->bottom - rect->top, + parent, controlID, hInstance, NULL); + if (hwnd == NULL) + // TODO return decoy window + logLastError(L"error creating D2D scratch window"); + if (SetWindowSubclass(hwnd, subclass, 0, subclassData) == FALSE) + logLastError(L"error subclassing D2D scratch window"); + return hwnd; +} diff --git a/src/libui_sdl/libui/windows/datetimepicker.cpp b/src/libui_sdl/libui/windows/datetimepicker.cpp new file mode 100644 index 0000000..e105c2f --- /dev/null +++ b/src/libui_sdl/libui/windows/datetimepicker.cpp @@ -0,0 +1,191 @@ +// 22 may 2015 +#include "uipriv_windows.hpp" + +struct uiDateTimePicker { + uiWindowsControl c; + HWND hwnd; +}; + +// utility functions + +#define GLI(what, buf, n) GetLocaleInfoEx(LOCALE_NAME_USER_DEFAULT, what, buf, n) + +// The real date/time picker does a manual replacement of "yy" with "yyyy" for DTS_SHORTDATECENTURYFORMAT. +// Because we're also duplicating its functionality (see below), we have to do it too. +static WCHAR *expandYear(WCHAR *dts, int n) +{ + WCHAR *out; + WCHAR *p, *q; + int ny = 0; + + // allocate more than we need to be safe + out = (WCHAR *) uiAlloc((n * 3) * sizeof (WCHAR), "WCHAR[]"); + q = out; + for (p = dts; *p != L'\0'; p++) { + // first, if the current character is a y, increment the number of consecutive ys + // otherwise, stop counting, and if there were only two, add two more to make four + if (*p != L'y') { + if (ny == 2) { + *q++ = L'y'; + *q++ = L'y'; + } + ny = 0; + } else + ny++; + // next, handle quoted blocks + // we do this AFTER the above so yy'abc' becomes yyyy'abc' and not yy'abc'yy + // this handles the case of 'a''b' elegantly as well + if (*p == L'\'') { + // copy the opening quote + *q++ = *p; + // copy the contents + for (;;) { + p++; + if (*p == L'\'') + break; + if (*p == L'\0') + implbug("unterminated quote in system-provided locale date string in expandYear()"); + *q++ = *p; + } + // and fall through to copy the closing quote + } + // copy the current character + *q++ = *p; + } + // handle trailing yy + if (ny == 2) { + *q++ = L'y'; + *q++ = L'y'; + } + *q++ = L'\0'; + return out; +} + +// Windows has no combined date/time prebuilt constant; we have to build the format string ourselves +// TODO use a default format if one fails +static void setDateTimeFormat(HWND hwnd) +{ + WCHAR *unexpandedDate, *date; + WCHAR *time; + WCHAR *datetime; + int ndate, ntime; + + ndate = GLI(LOCALE_SSHORTDATE, NULL, 0); + if (ndate == 0) + logLastError(L"error getting date string length"); + date = (WCHAR *) uiAlloc(ndate * sizeof (WCHAR), "WCHAR[]"); + if (GLI(LOCALE_SSHORTDATE, date, ndate) == 0) + logLastError(L"error geting date string"); + unexpandedDate = date; // so we can free it + date = expandYear(unexpandedDate, ndate); + uiFree(unexpandedDate); + + ntime = GLI(LOCALE_STIMEFORMAT, NULL, 0); + if (ndate == 0) + logLastError(L"error getting time string length"); + time = (WCHAR *) uiAlloc(ntime * sizeof (WCHAR), "WCHAR[]"); + if (GLI(LOCALE_STIMEFORMAT, time, ntime) == 0) + logLastError(L"error geting time string"); + + datetime = strf(L"%s %s", date, time); + if (SendMessageW(hwnd, DTM_SETFORMAT, 0, (LPARAM) datetime) == 0) + logLastError(L"error applying format string to date/time picker"); + + uiFree(datetime); + uiFree(time); + uiFree(date); +} + +// control implementation + +static void uiDateTimePickerDestroy(uiControl *c) +{ + uiDateTimePicker *d = uiDateTimePicker(c); + + uiWindowsUnregisterReceiveWM_WININICHANGE(d->hwnd); + uiWindowsEnsureDestroyWindow(d->hwnd); + uiFreeControl(uiControl(d)); +} + +uiWindowsControlAllDefaultsExceptDestroy(uiDateTimePicker) + +// the height returned from DTM_GETIDEALSIZE is unreliable; see http://stackoverflow.com/questions/30626549/what-is-the-proper-use-of-dtm-getidealsize-treating-the-returned-size-as-pixels +// from http://msdn.microsoft.com/en-us/library/windows/desktop/dn742486.aspx#sizingandspacing +#define entryHeight 14 + +static void uiDateTimePickerMinimumSize(uiWindowsControl *c, int *width, int *height) +{ + uiDateTimePicker *d = uiDateTimePicker(c); + SIZE s; + uiWindowsSizing sizing; + int y; + + s.cx = 0; + s.cy = 0; + SendMessageW(d->hwnd, DTM_GETIDEALSIZE, 0, (LPARAM) (&s)); + *width = s.cx; + + y = entryHeight; + uiWindowsGetSizing(d->hwnd, &sizing); + uiWindowsSizingDlgUnitsToPixels(&sizing, NULL, &y); + *height = y; +} + +static uiDateTimePicker *finishNewDateTimePicker(DWORD style) +{ + uiDateTimePicker *d; + + uiWindowsNewControl(uiDateTimePicker, d); + + d->hwnd = uiWindowsEnsureCreateControlHWND(WS_EX_CLIENTEDGE, + DATETIMEPICK_CLASSW, L"", + style | WS_TABSTOP, + hInstance, NULL, + TRUE); + + // automatically update date/time format when user changes locale settings + // for the standard styles, this is in the date-time picker itself + // for our date/time mode, we do it in a subclass assigned in uiNewDateTimePicker() + uiWindowsRegisterReceiveWM_WININICHANGE(d->hwnd); + + return d; +} + +static LRESULT CALLBACK datetimepickerSubProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) +{ + switch (uMsg) { + case WM_WININICHANGE: + // we can optimize this by only doing it when the real date/time picker does it + // unfortunately, I don't know when that is :/ + // hopefully this won't hurt + setDateTimeFormat(hwnd); + return 0; + case WM_NCDESTROY: + if (RemoveWindowSubclass(hwnd, datetimepickerSubProc, uIdSubclass) == FALSE) + logLastError(L"error removing date-time picker locale change handling subclass"); + break; + } + return DefSubclassProc(hwnd, uMsg, wParam, lParam); +} + +uiDateTimePicker *uiNewDateTimePicker(void) +{ + uiDateTimePicker *d; + + d = finishNewDateTimePicker(0); + setDateTimeFormat(d->hwnd); + if (SetWindowSubclass(d->hwnd, datetimepickerSubProc, 0, (DWORD_PTR) d) == FALSE) + logLastError(L"error subclassing date-time-picker to assist in locale change handling"); + // TODO set a suitable default in this case + return d; +} + +uiDateTimePicker *uiNewDatePicker(void) +{ + return finishNewDateTimePicker(DTS_SHORTDATECENTURYFORMAT); +} + +uiDateTimePicker *uiNewTimePicker(void) +{ + return finishNewDateTimePicker(DTS_TIMEFORMAT); +} diff --git a/src/libui_sdl/libui/windows/debug.cpp b/src/libui_sdl/libui/windows/debug.cpp new file mode 100644 index 0000000..cfafffd --- /dev/null +++ b/src/libui_sdl/libui/windows/debug.cpp @@ -0,0 +1,84 @@ +// 25 february 2015 +#include "uipriv_windows.hpp" + +// LONGTERM disable logging and stopping on no-debug builds + +static void printDebug(const WCHAR *msg) +{ + OutputDebugStringW(msg); +} + +HRESULT _logLastError(debugargs, const WCHAR *s) +{ + DWORD le; + WCHAR *msg; + WCHAR *formatted; + BOOL useFormatted; + + le = GetLastError(); + + useFormatted = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, le, 0, (LPWSTR) (&formatted), 0, NULL) != 0; + if (!useFormatted) + formatted = L"\n"; + msg = strf(L"[libui] %s:%s:%s() %s: GetLastError() == %I32u %s", + file, line, func, + s, le, formatted); + if (useFormatted) + LocalFree(formatted); // ignore error + printDebug(msg); + uiFree(msg); + DebugBreak(); + + SetLastError(le); + // a function does not have to set a last error + // if the last error we get is actually 0, then HRESULT_FROM_WIN32(0) will return S_OK (0 cast to an HRESULT, since 0 <= 0), which we don't want + // prevent this by returning E_FAIL + if (le == 0) + return E_FAIL; + return HRESULT_FROM_WIN32(le); +} + +HRESULT _logHRESULT(debugargs, const WCHAR *s, HRESULT hr) +{ + WCHAR *msg; + WCHAR *formatted; + BOOL useFormatted; + + useFormatted = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, hr, 0, (LPWSTR) (&formatted), 0, NULL) != 0; + if (!useFormatted) + formatted = L"\n"; + msg = strf(L"[libui] %s:%s:%s() %s: HRESULT == 0x%08I32X %s", + file, line, func, + s, hr, formatted); + if (useFormatted) + LocalFree(formatted); // ignore error + printDebug(msg); + uiFree(msg); + DebugBreak(); + + return hr; +} + +void realbug(const char *file, const char *line, const char *func, const char *prefix, const char *format, va_list ap) +{ + va_list ap2; + char *msg; + size_t n; + WCHAR *final; + + va_copy(ap2, ap); + n = _vscprintf(format, ap2); + va_end(ap2); + n++; // terminating '\0' + + msg = (char *) uiAlloc(n * sizeof (char), "char[]"); + // includes terminating '\0' according to example in https://msdn.microsoft.com/en-us/library/xa1a1a6z.aspx + vsprintf_s(msg, n, format, ap); + + final = strf(L"[libui] %hs:%hs:%hs() %hs%hs\n", file, line, func, prefix, msg); + uiFree(msg); + printDebug(final); + uiFree(final); + + DebugBreak(); +} diff --git a/src/libui_sdl/libui/windows/draw.cpp b/src/libui_sdl/libui/windows/draw.cpp new file mode 100644 index 0000000..5f4d29f --- /dev/null +++ b/src/libui_sdl/libui/windows/draw.cpp @@ -0,0 +1,511 @@ +// 7 september 2015 +#include "uipriv_windows.hpp" +#include "draw.hpp" + +ID2D1Factory *d2dfactory = NULL; + +HRESULT initDraw(void) +{ + D2D1_FACTORY_OPTIONS opts; + + ZeroMemory(&opts, sizeof (D2D1_FACTORY_OPTIONS)); + // TODO make this an option + opts.debugLevel = D2D1_DEBUG_LEVEL_NONE; + return D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, + IID_ID2D1Factory, + &opts, + (void **) (&d2dfactory)); +} + +void uninitDraw(void) +{ + d2dfactory->Release(); +} + +ID2D1HwndRenderTarget *makeHWNDRenderTarget(HWND hwnd) +{ + D2D1_RENDER_TARGET_PROPERTIES props; + D2D1_HWND_RENDER_TARGET_PROPERTIES hprops; + HDC dc; + RECT r; + ID2D1HwndRenderTarget *rt; + HRESULT hr; + + // we need a DC for the DPI + // we *could* just use the screen DPI but why when we have a window handle and its DC has a DPI + dc = GetDC(hwnd); + if (dc == NULL) + logLastError(L"error getting DC to find DPI"); + + ZeroMemory(&props, sizeof (D2D1_RENDER_TARGET_PROPERTIES)); + props.type = D2D1_RENDER_TARGET_TYPE_DEFAULT; + props.pixelFormat.format = DXGI_FORMAT_UNKNOWN; + props.pixelFormat.alphaMode = D2D1_ALPHA_MODE_UNKNOWN; + props.dpiX = GetDeviceCaps(dc, LOGPIXELSX); + props.dpiY = GetDeviceCaps(dc, LOGPIXELSY); + props.usage = D2D1_RENDER_TARGET_USAGE_NONE; + props.minLevel = D2D1_FEATURE_LEVEL_DEFAULT; + + if (ReleaseDC(hwnd, dc) == 0) + logLastError(L"error releasing DC for finding DPI"); + + uiWindowsEnsureGetClientRect(hwnd, &r); + + ZeroMemory(&hprops, sizeof (D2D1_HWND_RENDER_TARGET_PROPERTIES)); + hprops.hwnd = hwnd; + hprops.pixelSize.width = r.right - r.left; + hprops.pixelSize.height = r.bottom - r.top; + // according to Rick Brewster, some drivers will misbehave if we don't specify this (see http://stackoverflow.com/a/33222983/3408572) + hprops.presentOptions = D2D1_PRESENT_OPTIONS_RETAIN_CONTENTS; + + hr = d2dfactory->CreateHwndRenderTarget( + &props, + &hprops, + &rt); + if (hr != S_OK) + logHRESULT(L"error creating HWND render target", hr); + return rt; +} + +ID2D1DCRenderTarget *makeHDCRenderTarget(HDC dc, RECT *r) +{ + D2D1_RENDER_TARGET_PROPERTIES props; + ID2D1DCRenderTarget *rt; + HRESULT hr; + + ZeroMemory(&props, sizeof (D2D1_RENDER_TARGET_PROPERTIES)); + props.type = D2D1_RENDER_TARGET_TYPE_DEFAULT; + props.pixelFormat.format = DXGI_FORMAT_B8G8R8A8_UNORM; + props.pixelFormat.alphaMode = D2D1_ALPHA_MODE_PREMULTIPLIED; + props.dpiX = GetDeviceCaps(dc, LOGPIXELSX); + props.dpiY = GetDeviceCaps(dc, LOGPIXELSY); + props.usage = D2D1_RENDER_TARGET_USAGE_GDI_COMPATIBLE; + props.minLevel = D2D1_FEATURE_LEVEL_DEFAULT; + + hr = d2dfactory->CreateDCRenderTarget(&props, &rt); + if (hr != S_OK) + logHRESULT(L"error creating DC render target", hr); + hr = rt->BindDC(dc, r); + if (hr != S_OK) + logHRESULT(L"error binding DC to DC render target", hr); + return rt; +} + +static void resetTarget(ID2D1RenderTarget *rt) +{ + D2D1_MATRIX_3X2_F dm; + + // transformations persist + // reset to the identity matrix + ZeroMemory(&dm, sizeof (D2D1_MATRIX_3X2_F)); + dm._11 = 1; + dm._22 = 1; + rt->SetTransform(&dm); +} + +uiDrawContext *newContext(ID2D1RenderTarget *rt) +{ + uiDrawContext *c; + + c = uiNew(uiDrawContext); + c->rt = rt; + c->states = new std::vector<struct drawState>; + resetTarget(c->rt); + return c; +} + +void freeContext(uiDrawContext *c) +{ + if (c->currentClip != NULL) + c->currentClip->Release(); + if (c->states->size() != 0) + // TODO do this on other platforms + userbug("You did not balance uiDrawSave() and uiDrawRestore() calls."); + delete c->states; + uiFree(c); +} + +static ID2D1Brush *makeSolidBrush(uiDrawBrush *b, ID2D1RenderTarget *rt, D2D1_BRUSH_PROPERTIES *props) +{ + D2D1_COLOR_F color; + ID2D1SolidColorBrush *brush; + HRESULT hr; + + color.r = b->R; + color.g = b->G; + color.b = b->B; + color.a = b->A; + + hr = rt->CreateSolidColorBrush( + &color, + props, + &brush); + if (hr != S_OK) + logHRESULT(L"error creating solid brush", hr); + return brush; +} + +static ID2D1GradientStopCollection *mkstops(uiDrawBrush *b, ID2D1RenderTarget *rt) +{ + ID2D1GradientStopCollection *s; + D2D1_GRADIENT_STOP *stops; + size_t i; + HRESULT hr; + + stops = (D2D1_GRADIENT_STOP *) uiAlloc(b->NumStops * sizeof (D2D1_GRADIENT_STOP), "D2D1_GRADIENT_STOP[]"); + for (i = 0; i < b->NumStops; i++) { + stops[i].position = b->Stops[i].Pos; + stops[i].color.r = b->Stops[i].R; + stops[i].color.g = b->Stops[i].G; + stops[i].color.b = b->Stops[i].B; + stops[i].color.a = b->Stops[i].A; + } + + hr = rt->CreateGradientStopCollection( + stops, + b->NumStops, + D2D1_GAMMA_2_2, // this is the default for the C++-only overload of ID2D1RenderTarget::GradientStopCollection() + D2D1_EXTEND_MODE_CLAMP, + &s); + if (hr != S_OK) + logHRESULT(L"error creating stop collection", hr); + + uiFree(stops); + return s; +} + +static ID2D1Brush *makeLinearBrush(uiDrawBrush *b, ID2D1RenderTarget *rt, D2D1_BRUSH_PROPERTIES *props) +{ + ID2D1LinearGradientBrush *brush; + D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES gprops; + ID2D1GradientStopCollection *stops; + HRESULT hr; + + ZeroMemory(&gprops, sizeof (D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES)); + gprops.startPoint.x = b->X0; + gprops.startPoint.y = b->Y0; + gprops.endPoint.x = b->X1; + gprops.endPoint.y = b->Y1; + + stops = mkstops(b, rt); + + hr = rt->CreateLinearGradientBrush( + &gprops, + props, + stops, + &brush); + if (hr != S_OK) + logHRESULT(L"error creating gradient brush", hr); + + // the example at https://msdn.microsoft.com/en-us/library/windows/desktop/dd756682%28v=vs.85%29.aspx says this is safe to do now + stops->Release(); + return brush; +} + +static ID2D1Brush *makeRadialBrush(uiDrawBrush *b, ID2D1RenderTarget *rt, D2D1_BRUSH_PROPERTIES *props) +{ + ID2D1RadialGradientBrush *brush; + D2D1_RADIAL_GRADIENT_BRUSH_PROPERTIES gprops; + ID2D1GradientStopCollection *stops; + HRESULT hr; + + ZeroMemory(&gprops, sizeof (D2D1_RADIAL_GRADIENT_BRUSH_PROPERTIES)); + gprops.gradientOriginOffset.x = b->X0 - b->X1; + gprops.gradientOriginOffset.y = b->Y0 - b->Y1; + gprops.center.x = b->X1; + gprops.center.y = b->Y1; + gprops.radiusX = b->OuterRadius; + gprops.radiusY = b->OuterRadius; + + stops = mkstops(b, rt); + + hr = rt->CreateRadialGradientBrush( + &gprops, + props, + stops, + &brush); + if (hr != S_OK) + logHRESULT(L"error creating gradient brush", hr); + + stops->Release(); + return brush; +} + +static ID2D1Brush *makeBrush(uiDrawBrush *b, ID2D1RenderTarget *rt) +{ + D2D1_BRUSH_PROPERTIES props; + + ZeroMemory(&props, sizeof (D2D1_BRUSH_PROPERTIES)); + props.opacity = 1.0; + // identity matrix + props.transform._11 = 1; + props.transform._22 = 1; + + switch (b->Type) { + case uiDrawBrushTypeSolid: + return makeSolidBrush(b, rt, &props); + case uiDrawBrushTypeLinearGradient: + return makeLinearBrush(b, rt, &props); + case uiDrawBrushTypeRadialGradient: + return makeRadialBrush(b, rt, &props); +// case uiDrawBrushTypeImage: +// TODO + } + + // TODO do this on all platforms + userbug("Invalid brush type %d given to drawing operation.", b->Type); + // TODO dummy brush? + return NULL; // make compiler happy +} + +// how clipping works: +// every fill and stroke is done on a temporary layer with the clip geometry applied to it +// this is really the only way to clip in Direct2D that doesn't involve opacity images +// reference counting: +// - initially the clip is NULL, which means do not use a layer +// - the first time uiDrawClip() is called, we take a reference on the path passed in (this is also why uiPathEnd() is needed) +// - every successive time, we create a new PathGeometry and merge the current clip with the new path, releasing the reference we took earlier and taking a reference to the new one +// - in Save, we take another reference; in Restore we drop the refernece to the existing path geometry and transfer that saved ref to the new path geometry over to the context +// uiDrawFreePath() doesn't destroy the path geometry, it just drops the reference count, so a clip can exist independent of its path + +static ID2D1Layer *applyClip(uiDrawContext *c) +{ + ID2D1Layer *layer; + D2D1_LAYER_PARAMETERS params; + HRESULT hr; + + // if no clip, don't do anything + if (c->currentClip == NULL) + return NULL; + + // create a layer for clipping + // we have to explicitly make the layer because we're still targeting Windows 7 + hr = c->rt->CreateLayer(NULL, &layer); + if (hr != S_OK) + logHRESULT(L"error creating clip layer", hr); + + // apply it as the clip + ZeroMemory(¶ms, sizeof (D2D1_LAYER_PARAMETERS)); + // this is the equivalent of InfiniteRect() in d2d1helper.h + params.contentBounds.left = -FLT_MAX; + params.contentBounds.top = -FLT_MAX; + params.contentBounds.right = FLT_MAX; + params.contentBounds.bottom = FLT_MAX; + params.geometricMask = (ID2D1Geometry *) (c->currentClip); + // TODO is this correct? + params.maskAntialiasMode = c->rt->GetAntialiasMode(); + // identity matrix + params.maskTransform._11 = 1; + params.maskTransform._22 = 1; + params.opacity = 1.0; + params.opacityBrush = NULL; + params.layerOptions = D2D1_LAYER_OPTIONS_NONE; + // TODO is this correct? + if (c->rt->GetTextAntialiasMode() == D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE) + params.layerOptions = D2D1_LAYER_OPTIONS_INITIALIZE_FOR_CLEARTYPE; + c->rt->PushLayer(¶ms, layer); + + // return the layer so it can be freed later + return layer; +} + +static void unapplyClip(uiDrawContext *c, ID2D1Layer *layer) +{ + if (layer == NULL) + return; + c->rt->PopLayer(); + layer->Release(); +} + +void uiDrawStroke(uiDrawContext *c, uiDrawPath *p, uiDrawBrush *b, uiDrawStrokeParams *sp) +{ + ID2D1Brush *brush; + ID2D1StrokeStyle *style; + D2D1_STROKE_STYLE_PROPERTIES dsp; + FLOAT *dashes; + size_t i; + ID2D1Layer *cliplayer; + HRESULT hr; + + brush = makeBrush(b, c->rt); + + ZeroMemory(&dsp, sizeof (D2D1_STROKE_STYLE_PROPERTIES)); + switch (sp->Cap) { + case uiDrawLineCapFlat: + dsp.startCap = D2D1_CAP_STYLE_FLAT; + dsp.endCap = D2D1_CAP_STYLE_FLAT; + dsp.dashCap = D2D1_CAP_STYLE_FLAT; + break; + case uiDrawLineCapRound: + dsp.startCap = D2D1_CAP_STYLE_ROUND; + dsp.endCap = D2D1_CAP_STYLE_ROUND; + dsp.dashCap = D2D1_CAP_STYLE_ROUND; + break; + case uiDrawLineCapSquare: + dsp.startCap = D2D1_CAP_STYLE_SQUARE; + dsp.endCap = D2D1_CAP_STYLE_SQUARE; + dsp.dashCap = D2D1_CAP_STYLE_SQUARE; + break; + } + switch (sp->Join) { + case uiDrawLineJoinMiter: + dsp.lineJoin = D2D1_LINE_JOIN_MITER_OR_BEVEL; + dsp.miterLimit = sp->MiterLimit; + break; + case uiDrawLineJoinRound: + dsp.lineJoin = D2D1_LINE_JOIN_ROUND; + break; + case uiDrawLineJoinBevel: + dsp.lineJoin = D2D1_LINE_JOIN_BEVEL; + break; + } + dsp.dashStyle = D2D1_DASH_STYLE_SOLID; + dashes = NULL; + // note that dash widths and the dash phase are scaled up by the thickness by Direct2D + // TODO be sure to formally document this + if (sp->NumDashes != 0) { + dsp.dashStyle = D2D1_DASH_STYLE_CUSTOM; + dashes = (FLOAT *) uiAlloc(sp->NumDashes * sizeof (FLOAT), "FLOAT[]"); + for (i = 0; i < sp->NumDashes; i++) + dashes[i] = sp->Dashes[i] / sp->Thickness; + } + dsp.dashOffset = sp->DashPhase / sp->Thickness; + hr = d2dfactory->CreateStrokeStyle( + &dsp, + dashes, + sp->NumDashes, + &style); + if (hr != S_OK) + logHRESULT(L"error creating stroke style", hr); + if (sp->NumDashes != 0) + uiFree(dashes); + + cliplayer = applyClip(c); + c->rt->DrawGeometry( + pathGeometry(p), + brush, + sp->Thickness, + style); + unapplyClip(c, cliplayer); + + style->Release(); + brush->Release(); +} + +void uiDrawFill(uiDrawContext *c, uiDrawPath *p, uiDrawBrush *b) +{ + ID2D1Brush *brush; + ID2D1Layer *cliplayer; + + brush = makeBrush(b, c->rt); + cliplayer = applyClip(c); + c->rt->FillGeometry( + pathGeometry(p), + brush, + NULL); + unapplyClip(c, cliplayer); + brush->Release(); +} + +void uiDrawTransform(uiDrawContext *c, uiDrawMatrix *m) +{ + D2D1_MATRIX_3X2_F dm, cur; + + c->rt->GetTransform(&cur); + m2d(m, &dm); + // you would think we have to do already * m, right? + // WRONG! we have to do m * already + // why? a few reasons + // a) this lovely comment in cairo's source - http://cgit.freedesktop.org/cairo/tree/src/cairo-matrix.c?id=0537479bd1d4c5a3bc0f6f41dec4deb98481f34a#n330 + // Direct2D uses column vectors and I don't know if this is even documented + // b) that's what Core Graphics does + // TODO see if Microsoft says to do this + dm = dm * cur; // for whatever reason operator * is defined but not operator *= + c->rt->SetTransform(&dm); +} + +void uiDrawClip(uiDrawContext *c, uiDrawPath *path) +{ + ID2D1PathGeometry *newPath; + ID2D1GeometrySink *newSink; + HRESULT hr; + + // if there's no current clip, borrow the path + if (c->currentClip == NULL) { + c->currentClip = pathGeometry(path); + // we have to take our own reference to that clip + c->currentClip->AddRef(); + return; + } + + // otherwise we have to intersect the current path with the new one + // we do that into a new path, and then replace c->currentClip with that new path + hr = d2dfactory->CreatePathGeometry(&newPath); + if (hr != S_OK) + logHRESULT(L"error creating new path", hr); + hr = newPath->Open(&newSink); + if (hr != S_OK) + logHRESULT(L"error opening new path", hr); + hr = c->currentClip->CombineWithGeometry( + pathGeometry(path), + D2D1_COMBINE_MODE_INTERSECT, + NULL, + // TODO is this correct or can this be set per target? + D2D1_DEFAULT_FLATTENING_TOLERANCE, + newSink); + if (hr != S_OK) + logHRESULT(L"error intersecting old path with new path", hr); + hr = newSink->Close(); + if (hr != S_OK) + logHRESULT(L"error closing new path", hr); + newSink->Release(); + + // okay we have the new clip; we just need to replace the old one with it + c->currentClip->Release(); + c->currentClip = newPath; + // we have a reference already; no need for another +} + +struct drawState { + ID2D1DrawingStateBlock *dsb; + ID2D1PathGeometry *clip; +}; + +void uiDrawSave(uiDrawContext *c) +{ + struct drawState state; + HRESULT hr; + + hr = d2dfactory->CreateDrawingStateBlock( + // TODO verify that these are correct + NULL, + NULL, + &(state.dsb)); + if (hr != S_OK) + logHRESULT(L"error creating drawing state block", hr); + c->rt->SaveDrawingState(state.dsb); + + // if we have a clip, we need to hold another reference to it + if (c->currentClip != NULL) + c->currentClip->AddRef(); + state.clip = c->currentClip; // even if NULL assign it + + c->states->push_back(state); +} + +void uiDrawRestore(uiDrawContext *c) +{ + struct drawState state; + + state = (*(c->states))[c->states->size() - 1]; + c->states->pop_back(); + + c->rt->RestoreDrawingState(state.dsb); + state.dsb->Release(); + + // if we have a current clip, we need to drop it + if (c->currentClip != NULL) + c->currentClip->Release(); + // no need to explicitly addref or release; just transfer the ref + c->currentClip = state.clip; +} diff --git a/src/libui_sdl/libui/windows/draw.hpp b/src/libui_sdl/libui/windows/draw.hpp new file mode 100644 index 0000000..b015791 --- /dev/null +++ b/src/libui_sdl/libui/windows/draw.hpp @@ -0,0 +1,16 @@ +// 5 may 2016 + +// draw.cpp +extern ID2D1Factory *d2dfactory; +struct uiDrawContext { + ID2D1RenderTarget *rt; + // TODO find out how this works + std::vector<struct drawState> *states; + ID2D1PathGeometry *currentClip; +}; + +// drawpath.cpp +extern ID2D1PathGeometry *pathGeometry(uiDrawPath *p); + +// drawmatrix.cpp +extern void m2d(uiDrawMatrix *m, D2D1_MATRIX_3X2_F *d); diff --git a/src/libui_sdl/libui/windows/drawmatrix.cpp b/src/libui_sdl/libui/windows/drawmatrix.cpp new file mode 100644 index 0000000..090972a --- /dev/null +++ b/src/libui_sdl/libui/windows/drawmatrix.cpp @@ -0,0 +1,117 @@ +// 7 september 2015 +#include "uipriv_windows.hpp" +#include "draw.hpp" + +void m2d(uiDrawMatrix *m, D2D1_MATRIX_3X2_F *d) +{ + d->_11 = m->M11; + d->_12 = m->M12; + d->_21 = m->M21; + d->_22 = m->M22; + d->_31 = m->M31; + d->_32 = m->M32; +} + +static void d2m(D2D1_MATRIX_3X2_F *d, uiDrawMatrix *m) +{ + m->M11 = d->_11; + m->M12 = d->_12; + m->M21 = d->_21; + m->M22 = d->_22; + m->M31 = d->_31; + m->M32 = d->_32; +} + +void uiDrawMatrixTranslate(uiDrawMatrix *m, double x, double y) +{ + D2D1_MATRIX_3X2_F dm; + + m2d(m, &dm); + dm = dm * D2D1::Matrix3x2F::Translation(x, y); + d2m(&dm, m); +} + +void uiDrawMatrixScale(uiDrawMatrix *m, double xCenter, double yCenter, double x, double y) +{ + D2D1_MATRIX_3X2_F dm; + D2D1_POINT_2F center; + + m2d(m, &dm); + center.x = xCenter; + center.y = yCenter; + dm = dm * D2D1::Matrix3x2F::Scale(x, y, center); + d2m(&dm, m); +} + +#define r2d(x) (x * (180.0 / uiPi)) + +void uiDrawMatrixRotate(uiDrawMatrix *m, double x, double y, double amount) +{ + D2D1_MATRIX_3X2_F dm; + D2D1_POINT_2F center; + + m2d(m, &dm); + center.x = x; + center.y = y; + dm = dm * D2D1::Matrix3x2F::Rotation(r2d(amount), center); + d2m(&dm, m); +} + +void uiDrawMatrixSkew(uiDrawMatrix *m, double x, double y, double xamount, double yamount) +{ + D2D1_MATRIX_3X2_F dm; + D2D1_POINT_2F center; + + m2d(m, &dm); + center.x = x; + center.y = y; + dm = dm * D2D1::Matrix3x2F::Skew(r2d(xamount), r2d(yamount), center); + d2m(&dm, m); +} + +void uiDrawMatrixMultiply(uiDrawMatrix *dest, uiDrawMatrix *src) +{ + D2D1_MATRIX_3X2_F c, d; + + m2d(dest, &c); + m2d(src, &d); + c = c * d; + d2m(&c, dest); +} + +int uiDrawMatrixInvertible(uiDrawMatrix *m) +{ + D2D1_MATRIX_3X2_F d; + + m2d(m, &d); + return D2D1IsMatrixInvertible(&d) != FALSE; +} + +int uiDrawMatrixInvert(uiDrawMatrix *m) +{ + D2D1_MATRIX_3X2_F d; + + m2d(m, &d); + if (D2D1InvertMatrix(&d) == FALSE) + return 0; + d2m(&d, m); + return 1; +} + +void uiDrawMatrixTransformPoint(uiDrawMatrix *m, double *x, double *y) +{ + D2D1::Matrix3x2F dm; + D2D1_POINT_2F pt; + + m2d(m, &dm); + pt.x = *x; + pt.y = *y; + pt = dm.TransformPoint(pt); + *x = pt.x; + *y = pt.y; +} + +void uiDrawMatrixTransformSize(uiDrawMatrix *m, double *x, double *y) +{ + fallbackTransformSize(m, x, y); +} diff --git a/src/libui_sdl/libui/windows/drawpath.cpp b/src/libui_sdl/libui/windows/drawpath.cpp new file mode 100644 index 0000000..49855be --- /dev/null +++ b/src/libui_sdl/libui/windows/drawpath.cpp @@ -0,0 +1,246 @@ +// 7 september 2015 +#include "uipriv_windows.hpp" +#include "draw.hpp" + +// TODO +// - write a test for transform followed by clip and clip followed by transform to make sure they work the same as on gtk+ and cocoa +// - write a test for nested transforms for gtk+ + +struct uiDrawPath { + ID2D1PathGeometry *path; + ID2D1GeometrySink *sink; + BOOL inFigure; +}; + +uiDrawPath *uiDrawNewPath(uiDrawFillMode fillmode) +{ + uiDrawPath *p; + HRESULT hr; + + p = uiNew(uiDrawPath); + hr = d2dfactory->CreatePathGeometry(&(p->path)); + if (hr != S_OK) + logHRESULT(L"error creating path", hr); + hr = p->path->Open(&(p->sink)); + if (hr != S_OK) + logHRESULT(L"error opening path", hr); + switch (fillmode) { + case uiDrawFillModeWinding: + p->sink->SetFillMode(D2D1_FILL_MODE_WINDING); + break; + case uiDrawFillModeAlternate: + p->sink->SetFillMode(D2D1_FILL_MODE_ALTERNATE); + break; + } + return p; +} + +void uiDrawFreePath(uiDrawPath *p) +{ + if (p->inFigure) + p->sink->EndFigure(D2D1_FIGURE_END_OPEN); + if (p->sink != NULL) + // TODO close sink first? + p->sink->Release(); + p->path->Release(); + uiFree(p); +} + +void uiDrawPathNewFigure(uiDrawPath *p, double x, double y) +{ + D2D1_POINT_2F pt; + + if (p->inFigure) + p->sink->EndFigure(D2D1_FIGURE_END_OPEN); + pt.x = x; + pt.y = y; + p->sink->BeginFigure(pt, D2D1_FIGURE_BEGIN_FILLED); + p->inFigure = TRUE; +} + +// Direct2D arcs require a little explanation. +// An arc in Direct2D is defined by the chord between the endpoints. +// There are four possible arcs with the same two endpoints that you can draw this way. +// See https://www.youtube.com/watch?v=ATS0ANW1UxQ for a demonstration. +// There is a property rotationAngle which deals with the rotation /of the entire ellipse that forms an ellpitical arc/ - it's effectively a transformation on the arc. +// That is to say, it's NOT THE SWEEP. +// The sweep is defined by the start and end points and whether the arc is "large". +// As a result, this design does not allow for full circles or ellipses with a single arc; they have to be simulated with two. + +struct arc { + double xCenter; + double yCenter; + double radius; + double startAngle; + double sweep; + int negative; +}; + +// this is used for the comparison below +// if it falls apart it can be changed later +#define aerMax 6 * DBL_EPSILON + +static void drawArc(uiDrawPath *p, struct arc *a, void (*startFunction)(uiDrawPath *, double, double)) +{ + double sinx, cosx; + double startX, startY; + double endX, endY; + D2D1_ARC_SEGMENT as; + BOOL fullCircle; + double absSweep; + + // as above, we can't do a full circle with one arc + // simulate it with two half-circles + // of course, we have a dragon: equality on floating-point values! + // I've chosen to do the AlmostEqualRelative() technique in https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/ + fullCircle = FALSE; + // use the absolute value to tackle both ≥2Ï€ and ≤-2Ï€ at the same time + absSweep = fabs(a->sweep); + if (absSweep > (2 * uiPi)) // this part is easy + fullCircle = TRUE; + else { + double aerDiff; + + aerDiff = fabs(absSweep - (2 * uiPi)); + // if we got here then we know a->sweep is larger (or the same!) + fullCircle = aerDiff <= absSweep * aerMax; + } + // TODO make sure this works right for the negative direction + if (fullCircle) { + a->sweep = uiPi; + drawArc(p, a, startFunction); + a->startAngle += uiPi; + drawArc(p, a, NULL); + return; + } + + // first, figure out the arc's endpoints + // unfortunately D2D1SinCos() is only defined on Windows 8 and newer + // the MSDN page doesn't say this, but says it requires d2d1_1.h, which is listed as only supported on Windows 8 and newer elsewhere on MSDN + // so we must use sin() and cos() and hope it's right... + sinx = sin(a->startAngle); + cosx = cos(a->startAngle); + startX = a->xCenter + a->radius * cosx; + startY = a->yCenter + a->radius * sinx; + sinx = sin(a->startAngle + a->sweep); + cosx = cos(a->startAngle + a->sweep); + endX = a->xCenter + a->radius * cosx; + endY = a->yCenter + a->radius * sinx; + + // now do the initial step to get the current point to be the start point + // this is either creating a new figure, drawing a line, or (in the case of our full circle code above) doing nothing + if (startFunction != NULL) + (*startFunction)(p, startX, startY); + + // now we can draw the arc + as.point.x = endX; + as.point.y = endY; + as.size.width = a->radius; + as.size.height = a->radius; + as.rotationAngle = 0; // as above, not relevant for circles + if (a->negative) + as.sweepDirection = D2D1_SWEEP_DIRECTION_COUNTER_CLOCKWISE; + else + as.sweepDirection = D2D1_SWEEP_DIRECTION_CLOCKWISE; + // TODO explain the outer if + if (!a->negative) + if (a->sweep > uiPi) + as.arcSize = D2D1_ARC_SIZE_LARGE; + else + as.arcSize = D2D1_ARC_SIZE_SMALL; + else + // TODO especially this part + if (a->sweep > uiPi) + as.arcSize = D2D1_ARC_SIZE_SMALL; + else + as.arcSize = D2D1_ARC_SIZE_LARGE; + p->sink->AddArc(&as); +} + +void uiDrawPathNewFigureWithArc(uiDrawPath *p, double xCenter, double yCenter, double radius, double startAngle, double sweep, int negative) +{ + struct arc a; + + a.xCenter = xCenter; + a.yCenter = yCenter; + a.radius = radius; + a.startAngle = startAngle; + a.sweep = sweep; + a.negative = negative; + drawArc(p, &a, uiDrawPathNewFigure); +} + +void uiDrawPathLineTo(uiDrawPath *p, double x, double y) +{ + D2D1_POINT_2F pt; + + pt.x = x; + pt.y = y; + p->sink->AddLine(pt); +} + +void uiDrawPathArcTo(uiDrawPath *p, double xCenter, double yCenter, double radius, double startAngle, double sweep, int negative) +{ + struct arc a; + + a.xCenter = xCenter; + a.yCenter = yCenter; + a.radius = radius; + a.startAngle = startAngle; + a.sweep = sweep; + a.negative = negative; + drawArc(p, &a, uiDrawPathLineTo); +} + +void uiDrawPathBezierTo(uiDrawPath *p, double c1x, double c1y, double c2x, double c2y, double endX, double endY) +{ + D2D1_BEZIER_SEGMENT s; + + s.point1.x = c1x; + s.point1.y = c1y; + s.point2.x = c2x; + s.point2.y = c2y; + s.point3.x = endX; + s.point3.y = endY; + p->sink->AddBezier(&s); +} + +void uiDrawPathCloseFigure(uiDrawPath *p) +{ + p->sink->EndFigure(D2D1_FIGURE_END_CLOSED); + p->inFigure = FALSE; +} + +void uiDrawPathAddRectangle(uiDrawPath *p, double x, double y, double width, double height) +{ + // this is the same algorithm used by cairo and Core Graphics, according to their documentations + uiDrawPathNewFigure(p, x, y); + uiDrawPathLineTo(p, x + width, y); + uiDrawPathLineTo(p, x + width, y + height); + uiDrawPathLineTo(p, x, y + height); + uiDrawPathCloseFigure(p); +} + +void uiDrawPathEnd(uiDrawPath *p) +{ + HRESULT hr; + + if (p->inFigure) { + p->sink->EndFigure(D2D1_FIGURE_END_OPEN); + // needed for uiDrawFreePath() + p->inFigure = FALSE; + } + hr = p->sink->Close(); + if (hr != S_OK) + logHRESULT(L"error closing path", hr); + p->sink->Release(); + // also needed for uiDrawFreePath() + p->sink = NULL; +} + +ID2D1PathGeometry *pathGeometry(uiDrawPath *p) +{ + if (p->sink != NULL) + userbug("You cannot draw with a uiDrawPath that was not ended. (path: %p)", p); + return p->path; +} diff --git a/src/libui_sdl/libui/windows/drawtext.cpp b/src/libui_sdl/libui/windows/drawtext.cpp new file mode 100644 index 0000000..05a24f6 --- /dev/null +++ b/src/libui_sdl/libui/windows/drawtext.cpp @@ -0,0 +1,531 @@ +// 22 december 2015 +#include "uipriv_windows.hpp" +#include "draw.hpp" +// TODO really migrate + +// notes: +// only available in windows 8 and newer: +// - character spacing +// - kerning control +// - justficiation (how could I possibly be making this up?!) +// - vertical text (SERIOUSLY?! WHAT THE ACTUAL FUCK, MICROSOFT?!?!?!? DID YOU NOT THINK ABOUT THIS THE FIRST TIME, TRYING TO IMPROVE THE INTERNATIONALIZATION OF WINDOWS 7?!?!?! bonus: some parts of MSDN even say 8.1 and up only!) + +struct uiDrawFontFamilies { + fontCollection *fc; +}; + +uiDrawFontFamilies *uiDrawListFontFamilies(void) +{ + struct uiDrawFontFamilies *ff; + + ff = uiNew(struct uiDrawFontFamilies); + ff->fc = loadFontCollection(); + return ff; +} + +int uiDrawFontFamiliesNumFamilies(uiDrawFontFamilies *ff) +{ + return ff->fc->fonts->GetFontFamilyCount(); +} + +char *uiDrawFontFamiliesFamily(uiDrawFontFamilies *ff, int n) +{ + IDWriteFontFamily *family; + WCHAR *wname; + char *name; + HRESULT hr; + + hr = ff->fc->fonts->GetFontFamily(n, &family); + if (hr != S_OK) + logHRESULT(L"error getting font out of collection", hr); + wname = fontCollectionFamilyName(ff->fc, family); + name = toUTF8(wname); + uiFree(wname); + family->Release(); + return name; +} + +void uiDrawFreeFontFamilies(uiDrawFontFamilies *ff) +{ + fontCollectionFree(ff->fc); + uiFree(ff); +} + +struct uiDrawTextFont { + IDWriteFont *f; + WCHAR *family; // save for convenience in uiDrawNewTextLayout() + double size; +}; + +uiDrawTextFont *mkTextFont(IDWriteFont *df, BOOL addRef, WCHAR *family, BOOL copyFamily, double size) +{ + uiDrawTextFont *font; + WCHAR *copy; + HRESULT hr; + + font = uiNew(uiDrawTextFont); + font->f = df; + if (addRef) + font->f->AddRef(); + if (copyFamily) { + copy = (WCHAR *) uiAlloc((wcslen(family) + 1) * sizeof (WCHAR), "WCHAR[]"); + wcscpy(copy, family); + font->family = copy; + } else + font->family = family; + font->size = size; + return font; +} + +// TODO consider moving these all to dwrite.cpp + +// TODO MinGW-w64 is missing this one +#define DWRITE_FONT_WEIGHT_SEMI_LIGHT (DWRITE_FONT_WEIGHT(350)) +static const struct { + bool lastOne; + uiDrawTextWeight uival; + DWRITE_FONT_WEIGHT dwval; +} dwriteWeights[] = { + { false, uiDrawTextWeightThin, DWRITE_FONT_WEIGHT_THIN }, + { false, uiDrawTextWeightUltraLight, DWRITE_FONT_WEIGHT_ULTRA_LIGHT }, + { false, uiDrawTextWeightLight, DWRITE_FONT_WEIGHT_LIGHT }, + { false, uiDrawTextWeightBook, DWRITE_FONT_WEIGHT_SEMI_LIGHT }, + { false, uiDrawTextWeightNormal, DWRITE_FONT_WEIGHT_NORMAL }, + { false, uiDrawTextWeightMedium, DWRITE_FONT_WEIGHT_MEDIUM }, + { false, uiDrawTextWeightSemiBold, DWRITE_FONT_WEIGHT_SEMI_BOLD }, + { false, uiDrawTextWeightBold, DWRITE_FONT_WEIGHT_BOLD }, + { false, uiDrawTextWeightUltraBold, DWRITE_FONT_WEIGHT_ULTRA_BOLD }, + { false, uiDrawTextWeightHeavy, DWRITE_FONT_WEIGHT_HEAVY }, + { true, uiDrawTextWeightUltraHeavy, DWRITE_FONT_WEIGHT_ULTRA_BLACK, }, +}; + +static const struct { + bool lastOne; + uiDrawTextItalic uival; + DWRITE_FONT_STYLE dwval; +} dwriteItalics[] = { + { false, uiDrawTextItalicNormal, DWRITE_FONT_STYLE_NORMAL }, + { false, uiDrawTextItalicOblique, DWRITE_FONT_STYLE_OBLIQUE }, + { true, uiDrawTextItalicItalic, DWRITE_FONT_STYLE_ITALIC }, +}; + +static const struct { + bool lastOne; + uiDrawTextStretch uival; + DWRITE_FONT_STRETCH dwval; +} dwriteStretches[] = { + { false, uiDrawTextStretchUltraCondensed, DWRITE_FONT_STRETCH_ULTRA_CONDENSED }, + { false, uiDrawTextStretchExtraCondensed, DWRITE_FONT_STRETCH_EXTRA_CONDENSED }, + { false, uiDrawTextStretchCondensed, DWRITE_FONT_STRETCH_CONDENSED }, + { false, uiDrawTextStretchSemiCondensed, DWRITE_FONT_STRETCH_SEMI_CONDENSED }, + { false, uiDrawTextStretchNormal, DWRITE_FONT_STRETCH_NORMAL }, + { false, uiDrawTextStretchSemiExpanded, DWRITE_FONT_STRETCH_SEMI_EXPANDED }, + { false, uiDrawTextStretchExpanded, DWRITE_FONT_STRETCH_EXPANDED }, + { false, uiDrawTextStretchExtraExpanded, DWRITE_FONT_STRETCH_EXTRA_EXPANDED }, + { true, uiDrawTextStretchUltraExpanded, DWRITE_FONT_STRETCH_ULTRA_EXPANDED }, +}; + +void attrToDWriteAttr(struct dwriteAttr *attr) +{ + bool found; + int i; + + found = false; + for (i = 0; ; i++) { + if (dwriteWeights[i].uival == attr->weight) { + attr->dweight = dwriteWeights[i].dwval; + found = true; + break; + } + if (dwriteWeights[i].lastOne) + break; + } + if (!found) + userbug("Invalid text weight %d passed to text function.", attr->weight); + + found = false; + for (i = 0; ; i++) { + if (dwriteItalics[i].uival == attr->italic) { + attr->ditalic = dwriteItalics[i].dwval; + found = true; + break; + } + if (dwriteItalics[i].lastOne) + break; + } + if (!found) + userbug("Invalid text italic %d passed to text function.", attr->italic); + + found = false; + for (i = 0; ; i++) { + if (dwriteStretches[i].uival == attr->stretch) { + attr->dstretch = dwriteStretches[i].dwval; + found = true; + break; + } + if (dwriteStretches[i].lastOne) + break; + } + if (!found) + // TODO on other platforms too + userbug("Invalid text stretch %d passed to text function.", attr->stretch); +} + +void dwriteAttrToAttr(struct dwriteAttr *attr) +{ + int weight, against, n; + int curdiff, curindex; + bool found; + int i; + + // weight is scaled; we need to test to see what's nearest + weight = (int) (attr->dweight); + against = (int) (dwriteWeights[0].dwval); + curdiff = abs(against - weight); + curindex = 0; + for (i = 1; ; i++) { + against = (int) (dwriteWeights[i].dwval); + n = abs(against - weight); + if (n < curdiff) { + curdiff = n; + curindex = i; + } + if (dwriteWeights[i].lastOne) + break; + } + attr->weight = dwriteWeights[i].uival; + + // italic and stretch are simple values; we can just do a matching search + found = false; + for (i = 0; ; i++) { + if (dwriteItalics[i].dwval == attr->ditalic) { + attr->italic = dwriteItalics[i].uival; + found = true; + break; + } + if (dwriteItalics[i].lastOne) + break; + } + if (!found) + // these are implbug()s because users shouldn't be able to get here directly; TODO? + implbug("invalid italic %d passed to dwriteAttrToAttr()", attr->ditalic); + + found = false; + for (i = 0; ; i++) { + if (dwriteStretches[i].dwval == attr->dstretch) { + attr->stretch = dwriteStretches[i].uival; + found = true; + break; + } + if (dwriteStretches[i].lastOne) + break; + } + if (!found) + implbug("invalid stretch %d passed to dwriteAttrToAttr()", attr->dstretch); +} + +uiDrawTextFont *uiDrawLoadClosestFont(const uiDrawTextFontDescriptor *desc) +{ + uiDrawTextFont *font; + IDWriteFontCollection *collection; + UINT32 index; + BOOL exists; + struct dwriteAttr attr; + IDWriteFontFamily *family; + WCHAR *wfamily; + IDWriteFont *match; + HRESULT hr; + + // always get the latest available font information + hr = dwfactory->GetSystemFontCollection(&collection, TRUE); + if (hr != S_OK) + logHRESULT(L"error getting system font collection", hr); + + wfamily = toUTF16(desc->Family); + hr = collection->FindFamilyName(wfamily, &index, &exists); + if (hr != S_OK) + logHRESULT(L"error finding font family", hr); + if (!exists) + implbug("LONGTERM family not found in uiDrawLoadClosestFont()", hr); + hr = collection->GetFontFamily(index, &family); + if (hr != S_OK) + logHRESULT(L"error loading font family", hr); + + attr.weight = desc->Weight; + attr.italic = desc->Italic; + attr.stretch = desc->Stretch; + attrToDWriteAttr(&attr); + hr = family->GetFirstMatchingFont( + attr.dweight, + attr.dstretch, + attr.ditalic, + &match); + if (hr != S_OK) + logHRESULT(L"error loading font", hr); + + font = mkTextFont(match, + FALSE, // we own the initial reference; no need to add another one + wfamily, FALSE, // will be freed with font + desc->Size); + + family->Release(); + collection->Release(); + + return font; +} + +void uiDrawFreeTextFont(uiDrawTextFont *font) +{ + font->f->Release(); + uiFree(font->family); + uiFree(font); +} + +uintptr_t uiDrawTextFontHandle(uiDrawTextFont *font) +{ + return (uintptr_t) (font->f); +} + +void uiDrawTextFontDescribe(uiDrawTextFont *font, uiDrawTextFontDescriptor *desc) +{ + // TODO + + desc->Size = font->size; + + // TODO +} + +// text sizes are 1/72 of an inch +// points in Direct2D are 1/96 of an inch (https://msdn.microsoft.com/en-us/library/windows/desktop/ff684173%28v=vs.85%29.aspx, https://msdn.microsoft.com/en-us/library/windows/desktop/hh447022%28v=vs.85%29.aspx) +// As for the actual conversion from design units, see: +// - http://cboard.cprogramming.com/windows-programming/136733-directwrite-font-height-issues.html +// - https://sourceforge.net/p/vstgui/mailman/message/32483143/ +// - http://xboxforums.create.msdn.com/forums/t/109445.aspx +// - https://msdn.microsoft.com/en-us/library/dd183564%28v=vs.85%29.aspx +// - http://www.fontbureau.com/blog/the-em/ +// TODO make points here about how DIPs in DirectWrite == DIPs in Direct2D; if not, figure out what they really are? for the width and layout functions later +static double scaleUnits(double what, double designUnitsPerEm, double size) +{ + return (what / designUnitsPerEm) * (size * (96.0 / 72.0)); +} + +void uiDrawTextFontGetMetrics(uiDrawTextFont *font, uiDrawTextFontMetrics *metrics) +{ + DWRITE_FONT_METRICS dm; + + font->f->GetMetrics(&dm); + metrics->Ascent = scaleUnits(dm.ascent, dm.designUnitsPerEm, font->size); + metrics->Descent = scaleUnits(dm.descent, dm.designUnitsPerEm, font->size); + // TODO what happens if dm.xxx is negative? + // TODO remember what this was for + metrics->Leading = scaleUnits(dm.lineGap, dm.designUnitsPerEm, font->size); + metrics->UnderlinePos = scaleUnits(dm.underlinePosition, dm.designUnitsPerEm, font->size); + metrics->UnderlineThickness = scaleUnits(dm.underlineThickness, dm.designUnitsPerEm, font->size); +} + +// some attributes, such as foreground color, can't be applied until after we establish a Direct2D context :/ so we have to prepare all attributes in advance +// also since there's no way to clear the attributes from a layout en masse (apart from overwriting them all), we'll play it safe by creating a new layout each time +enum layoutAttrType { + layoutAttrColor, +}; + +struct layoutAttr { + enum layoutAttrType type; + int start; + int end; + double components[4]; +}; + +struct uiDrawTextLayout { + WCHAR *text; + size_t textlen; + size_t *graphemes; + double width; + IDWriteTextFormat *format; + std::vector<struct layoutAttr> *attrs; +}; + +uiDrawTextLayout *uiDrawNewTextLayout(const char *text, uiDrawTextFont *defaultFont, double width) +{ + uiDrawTextLayout *layout; + HRESULT hr; + + layout = uiNew(uiDrawTextLayout); + + hr = dwfactory->CreateTextFormat(defaultFont->family, + NULL, + defaultFont->f->GetWeight(), + defaultFont->f->GetStyle(), + defaultFont->f->GetStretch(), + // typographic points are 1/72 inch; this parameter is 1/96 inch + // fortunately Microsoft does this too, in https://msdn.microsoft.com/en-us/library/windows/desktop/dd371554%28v=vs.85%29.aspx + defaultFont->size * (96.0 / 72.0), + // see http://stackoverflow.com/questions/28397971/idwritefactorycreatetextformat-failing and https://msdn.microsoft.com/en-us/library/windows/desktop/dd368203.aspx + // TODO use the current locale again? + L"", + &(layout->format)); + if (hr != S_OK) + logHRESULT(L"error creating IDWriteTextFormat", hr); + + layout->text = toUTF16(text); + layout->textlen = wcslen(layout->text); + layout->graphemes = graphemes(layout->text); + + uiDrawTextLayoutSetWidth(layout, width); + + layout->attrs = new std::vector<struct layoutAttr>; + + return layout; +} + +void uiDrawFreeTextLayout(uiDrawTextLayout *layout) +{ + delete layout->attrs; + layout->format->Release(); + uiFree(layout->graphemes); + uiFree(layout->text); + uiFree(layout); +} + +static ID2D1SolidColorBrush *mkSolidBrush(ID2D1RenderTarget *rt, double r, double g, double b, double a) +{ + D2D1_BRUSH_PROPERTIES props; + D2D1_COLOR_F color; + ID2D1SolidColorBrush *brush; + HRESULT hr; + + ZeroMemory(&props, sizeof (D2D1_BRUSH_PROPERTIES)); + props.opacity = 1.0; + // identity matrix + props.transform._11 = 1; + props.transform._22 = 1; + color.r = r; + color.g = g; + color.b = b; + color.a = a; + hr = rt->CreateSolidColorBrush( + &color, + &props, + &brush); + if (hr != S_OK) + logHRESULT(L"error creating solid brush", hr); + return brush; +} + +IDWriteTextLayout *prepareLayout(uiDrawTextLayout *layout, ID2D1RenderTarget *rt) +{ + IDWriteTextLayout *dl; + DWRITE_TEXT_RANGE range; + IUnknown *unkBrush; + DWRITE_WORD_WRAPPING wrap; + FLOAT maxWidth; + HRESULT hr; + + hr = dwfactory->CreateTextLayout(layout->text, layout->textlen, + layout->format, + // FLOAT is float, not double, so this should work... TODO + FLT_MAX, FLT_MAX, + &dl); + if (hr != S_OK) + logHRESULT(L"error creating IDWriteTextLayout", hr); + + for (const struct layoutAttr &attr : *(layout->attrs)) { + range.startPosition = layout->graphemes[attr.start]; + range.length = layout->graphemes[attr.end] - layout->graphemes[attr.start]; + switch (attr.type) { + case layoutAttrColor: + if (rt == NULL) // determining extents, not drawing + break; + unkBrush = mkSolidBrush(rt, + attr.components[0], + attr.components[1], + attr.components[2], + attr.components[3]); + hr = dl->SetDrawingEffect(unkBrush, range); + unkBrush->Release(); // associated with dl + break; + default: + hr = E_FAIL; + logHRESULT(L"invalid text attribute type", hr); + } + if (hr != S_OK) + logHRESULT(L"error adding attribute to text layout", hr); + } + + // and set the width + // this is the only wrapping mode (apart from "no wrap") available prior to Windows 8.1 + wrap = DWRITE_WORD_WRAPPING_WRAP; + maxWidth = layout->width; + if (layout->width < 0) { + wrap = DWRITE_WORD_WRAPPING_NO_WRAP; + // setting the max width in this case technically isn't needed since the wrap mode will simply ignore the max width, but let's do it just to be safe + maxWidth = FLT_MAX; // see TODO above + } + hr = dl->SetWordWrapping(wrap); + if (hr != S_OK) + logHRESULT(L"error setting word wrapping mode", hr); + hr = dl->SetMaxWidth(maxWidth); + if (hr != S_OK) + logHRESULT(L"error setting max layout width", hr); + + return dl; +} + + +void uiDrawTextLayoutSetWidth(uiDrawTextLayout *layout, double width) +{ + layout->width = width; +} + +// TODO for a single line the height includes the leading; it should not +void uiDrawTextLayoutExtents(uiDrawTextLayout *layout, double *width, double *height) +{ + IDWriteTextLayout *dl; + DWRITE_TEXT_METRICS metrics; + HRESULT hr; + + dl = prepareLayout(layout, NULL); + hr = dl->GetMetrics(&metrics); + if (hr != S_OK) + logHRESULT(L"error getting layout metrics", hr); + *width = metrics.width; + // TODO make sure the behavior of this on empty strings is the same on all platforms + *height = metrics.height; + dl->Release(); +} + +void uiDrawText(uiDrawContext *c, double x, double y, uiDrawTextLayout *layout) +{ + IDWriteTextLayout *dl; + D2D1_POINT_2F pt; + ID2D1Brush *black; + HRESULT hr; + + // TODO document that fully opaque black is the default text color; figure out whether this is upheld in various scenarios on other platforms + black = mkSolidBrush(c->rt, 0.0, 0.0, 0.0, 1.0); + + dl = prepareLayout(layout, c->rt); + pt.x = x; + pt.y = y; + // TODO D2D1_DRAW_TEXT_OPTIONS_NO_SNAP? + // TODO D2D1_DRAW_TEXT_OPTIONS_CLIP? + // TODO when setting 8.1 as minimum, D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT? + c->rt->DrawTextLayout(pt, dl, black, D2D1_DRAW_TEXT_OPTIONS_NONE); + dl->Release(); + + black->Release(); +} + +void uiDrawTextLayoutSetColor(uiDrawTextLayout *layout, int startChar, int endChar, double r, double g, double b, double a) +{ + struct layoutAttr attr; + + attr.type = layoutAttrColor; + attr.start = startChar; + attr.end = endChar; + attr.components[0] = r; + attr.components[1] = g; + attr.components[2] = b; + attr.components[3] = a; + layout->attrs->push_back(attr); +} diff --git a/src/libui_sdl/libui/windows/dwrite.cpp b/src/libui_sdl/libui/windows/dwrite.cpp new file mode 100644 index 0000000..9156f17 --- /dev/null +++ b/src/libui_sdl/libui/windows/dwrite.cpp @@ -0,0 +1,88 @@ +// 14 april 2016 +#include "uipriv_windows.hpp" +// TODO really migrate? + +IDWriteFactory *dwfactory = NULL; + +HRESULT initDrawText(void) +{ + // TOOD use DWRITE_FACTORY_TYPE_ISOLATED instead? + return DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, + __uuidof (IDWriteFactory), + (IUnknown **) (&dwfactory)); +} + +void uninitDrawText(void) +{ + dwfactory->Release(); +} + +fontCollection *loadFontCollection(void) +{ + fontCollection *fc; + HRESULT hr; + + fc = uiNew(fontCollection); + // always get the latest available font information + hr = dwfactory->GetSystemFontCollection(&(fc->fonts), TRUE); + if (hr != S_OK) + logHRESULT(L"error getting system font collection", hr); + fc->userLocaleSuccess = GetUserDefaultLocaleName(fc->userLocale, LOCALE_NAME_MAX_LENGTH); + return fc; +} + +WCHAR *fontCollectionFamilyName(fontCollection *fc, IDWriteFontFamily *family) +{ + IDWriteLocalizedStrings *names; + WCHAR *str; + HRESULT hr; + + hr = family->GetFamilyNames(&names); + if (hr != S_OK) + logHRESULT(L"error getting names of font out", hr); + str = fontCollectionCorrectString(fc, names); + names->Release(); + return str; +} + +WCHAR *fontCollectionCorrectString(fontCollection *fc, IDWriteLocalizedStrings *names) +{ + UINT32 index; + BOOL exists; + UINT32 length; + WCHAR *wname; + HRESULT hr; + + // this is complex, but we ignore failure conditions to allow fallbacks + // 1) If the user locale name was successfully retrieved, try it + // 2) If the user locale name was not successfully retrieved, or that locale's string does not exist, or an error occurred, try L"en-us", the US English locale + // 3) And if that fails, assume the first one + // This algorithm is straight from MSDN: https://msdn.microsoft.com/en-us/library/windows/desktop/dd368214%28v=vs.85%29.aspx + // For step 2 to work, start by setting hr to S_OK and exists to FALSE. + // TODO does it skip step 2 entirely if step 1 fails? rewrite it to be a more pure conversion of the MSDN code? + hr = S_OK; + exists = FALSE; + if (fc->userLocaleSuccess != 0) + hr = names->FindLocaleName(fc->userLocale, &index, &exists); + if (hr != S_OK || (hr == S_OK && !exists)) + hr = names->FindLocaleName(L"en-us", &index, &exists); + if (!exists) + index = 0; + + hr = names->GetStringLength(index, &length); + if (hr != S_OK) + logHRESULT(L"error getting length of font name", hr); + // GetStringLength() does not include the null terminator, but GetString() does + wname = (WCHAR *) uiAlloc((length + 1) * sizeof (WCHAR), "WCHAR[]"); + hr = names->GetString(index, wname, length + 1); + if (hr != S_OK) + logHRESULT(L"error getting font name", hr); + + return wname; +} + +void fontCollectionFree(fontCollection *fc) +{ + fc->fonts->Release(); + uiFree(fc); +} diff --git a/src/libui_sdl/libui/windows/editablecombo.cpp b/src/libui_sdl/libui/windows/editablecombo.cpp new file mode 100644 index 0000000..9e1fdbf --- /dev/null +++ b/src/libui_sdl/libui/windows/editablecombo.cpp @@ -0,0 +1,115 @@ +// 20 may 2015 +#include "uipriv_windows.hpp" + +// we as Common Controls 6 users don't need to worry about the height of comboboxes; see http://blogs.msdn.com/b/oldnewthing/archive/2006/03/10/548537.aspx + +struct uiEditableCombobox { + uiWindowsControl c; + HWND hwnd; + void (*onChanged)(uiEditableCombobox *, void *); + void *onChangedData; +}; + +static BOOL onWM_COMMAND(uiControl *cc, HWND hwnd, WORD code, LRESULT *lResult) +{ + uiEditableCombobox *c = uiEditableCombobox(cc); + + if (code == CBN_SELCHANGE) { + // like on OS X, this is sent before the edit has been updated :( + if (PostMessage(parentOf(hwnd), + WM_COMMAND, + MAKEWPARAM(GetWindowLongPtrW(hwnd, GWLP_ID), CBN_EDITCHANGE), + (LPARAM) hwnd) == 0) + logLastError(L"error posting CBN_EDITCHANGE after CBN_SELCHANGE"); + *lResult = 0; + return TRUE; + } + if (code != CBN_EDITCHANGE) + return FALSE; + (*(c->onChanged))(c, c->onChangedData); + *lResult = 0; + return TRUE; +} + +void uiEditableComboboxDestroy(uiControl *cc) +{ + uiEditableCombobox *c = uiEditableCombobox(cc); + + uiWindowsUnregisterWM_COMMANDHandler(c->hwnd); + uiWindowsEnsureDestroyWindow(c->hwnd); + uiFreeControl(uiControl(c)); +} + +uiWindowsControlAllDefaultsExceptDestroy(uiEditableCombobox) + +// from http://msdn.microsoft.com/en-us/library/windows/desktop/dn742486.aspx#sizingandspacing +#define comboboxWidth 107 /* this is actually the shorter progress bar width, but Microsoft only indicates as wide as necessary; LONGTERM */ +#define comboboxHeight 14 /* LONGTERM: is this too high? */ + +static void uiEditableComboboxMinimumSize(uiWindowsControl *cc, int *width, int *height) +{ + uiEditableCombobox *c = uiEditableCombobox(cc); + uiWindowsSizing sizing; + int x, y; + + x = comboboxWidth; + y = comboboxHeight; + uiWindowsGetSizing(c->hwnd, &sizing); + uiWindowsSizingDlgUnitsToPixels(&sizing, &x, &y); + *width = x; + *height = y; +} + +static void defaultOnChanged(uiEditableCombobox *c, void *data) +{ + // do nothing +} + +void uiEditableComboboxAppend(uiEditableCombobox *c, const char *text) +{ + WCHAR *wtext; + LRESULT res; + + wtext = toUTF16(text); + res = SendMessageW(c->hwnd, CB_ADDSTRING, 0, (LPARAM) wtext); + if (res == (LRESULT) CB_ERR) + logLastError(L"error appending item to uiEditableCombobox"); + else if (res == (LRESULT) CB_ERRSPACE) + logLastError(L"memory exhausted appending item to uiEditableCombobox"); + uiFree(wtext); +} + +char *uiEditableComboboxText(uiEditableCombobox *c) +{ + return uiWindowsWindowText(c->hwnd); +} + +void uiEditableComboboxSetText(uiEditableCombobox *c, const char *text) +{ + // does not trigger any notifications + uiWindowsSetWindowText(c->hwnd, text); +} + +void uiEditableComboboxOnChanged(uiEditableCombobox *c, void (*f)(uiEditableCombobox *c, void *data), void *data) +{ + c->onChanged = f; + c->onChangedData = data; +} + +uiEditableCombobox *uiNewEditableCombobox(void) +{ + uiEditableCombobox *c; + + uiWindowsNewControl(uiEditableCombobox, c); + + c->hwnd = uiWindowsEnsureCreateControlHWND(WS_EX_CLIENTEDGE, + L"combobox", L"", + CBS_DROPDOWN | WS_TABSTOP, + hInstance, NULL, + TRUE); + + uiWindowsRegisterWM_COMMANDHandler(c->hwnd, onWM_COMMAND, uiControl(c)); + uiEditableComboboxOnChanged(c, defaultOnChanged, NULL); + + return c; +} diff --git a/src/libui_sdl/libui/windows/entry.cpp b/src/libui_sdl/libui/windows/entry.cpp new file mode 100644 index 0000000..a7a077f --- /dev/null +++ b/src/libui_sdl/libui/windows/entry.cpp @@ -0,0 +1,134 @@ +// 8 april 2015 +#include "uipriv_windows.hpp" + +struct uiEntry { + uiWindowsControl c; + HWND hwnd; + void (*onChanged)(uiEntry *, void *); + void *onChangedData; + BOOL inhibitChanged; +}; + +static BOOL onWM_COMMAND(uiControl *c, HWND hwnd, WORD code, LRESULT *lResult) +{ + uiEntry *e = uiEntry(c); + + if (code != EN_CHANGE) + return FALSE; + if (e->inhibitChanged) + return FALSE; + (*(e->onChanged))(e, e->onChangedData); + *lResult = 0; + return TRUE; +} + +static void uiEntryDestroy(uiControl *c) +{ + uiEntry *e = uiEntry(c); + + uiWindowsUnregisterWM_COMMANDHandler(e->hwnd); + uiWindowsEnsureDestroyWindow(e->hwnd); + uiFreeControl(uiControl(e)); +} + +uiWindowsControlAllDefaultsExceptDestroy(uiEntry) + +// from http://msdn.microsoft.com/en-us/library/windows/desktop/dn742486.aspx#sizingandspacing +#define entryWidth 107 /* this is actually the shorter progress bar width, but Microsoft only indicates as wide as necessary */ +#define entryHeight 14 + +static void uiEntryMinimumSize(uiWindowsControl *c, int *width, int *height) +{ + uiEntry *e = uiEntry(c); + uiWindowsSizing sizing; + int x, y; + + x = entryWidth; + y = entryHeight; + uiWindowsGetSizing(e->hwnd, &sizing); + uiWindowsSizingDlgUnitsToPixels(&sizing, &x, &y); + *width = x; + *height = y; +} + +static void defaultOnChanged(uiEntry *e, void *data) +{ + // do nothing +} + +char *uiEntryText(uiEntry *e) +{ + return uiWindowsWindowText(e->hwnd); +} + +void uiEntrySetText(uiEntry *e, const char *text) +{ + // doing this raises an EN_CHANGED + e->inhibitChanged = TRUE; + uiWindowsSetWindowText(e->hwnd, text); + e->inhibitChanged = FALSE; + // don't queue the control for resize; entry sizes are independent of their contents +} + +void uiEntryOnChanged(uiEntry *e, void (*f)(uiEntry *, void *), void *data) +{ + e->onChanged = f; + e->onChangedData = data; +} + +int uiEntryReadOnly(uiEntry *e) +{ + return (getStyle(e->hwnd) & ES_READONLY) != 0; +} + +void uiEntrySetReadOnly(uiEntry *e, int readonly) +{ + WPARAM ro; + + ro = (WPARAM) FALSE; + if (readonly) + ro = (WPARAM) TRUE; + if (SendMessage(e->hwnd, EM_SETREADONLY, ro, 0) == 0) + logLastError(L"error making uiEntry read-only"); +} + +static uiEntry *finishNewEntry(DWORD style) +{ + uiEntry *e; + + uiWindowsNewControl(uiEntry, e); + + e->hwnd = uiWindowsEnsureCreateControlHWND(WS_EX_CLIENTEDGE, + L"edit", L"", + style | ES_AUTOHSCROLL | ES_LEFT | ES_NOHIDESEL | WS_TABSTOP, + hInstance, NULL, + TRUE); + + uiWindowsRegisterWM_COMMANDHandler(e->hwnd, onWM_COMMAND, uiControl(e)); + uiEntryOnChanged(e, defaultOnChanged, NULL); + + return e; +} + +uiEntry *uiNewEntry(void) +{ + return finishNewEntry(0); +} + +uiEntry *uiNewPasswordEntry(void) +{ + return finishNewEntry(ES_PASSWORD); +} + +uiEntry *uiNewSearchEntry(void) +{ + uiEntry *e; + HRESULT hr; + + e = finishNewEntry(0); + // TODO this is from ThemeExplorer; is it documented anywhere? + // TODO SearchBoxEditComposited has no border + hr = SetWindowTheme(e->hwnd, L"SearchBoxEdit", NULL); + // TODO will hr be S_OK if themes are disabled? + return e; +} diff --git a/src/libui_sdl/libui/windows/events.cpp b/src/libui_sdl/libui/windows/events.cpp new file mode 100644 index 0000000..45e8d43 --- /dev/null +++ b/src/libui_sdl/libui/windows/events.cpp @@ -0,0 +1,151 @@ +// 20 may 2015 +#include "uipriv_windows.hpp" + +struct handler { + BOOL (*commandHandler)(uiControl *, HWND, WORD, LRESULT *); + BOOL (*notifyHandler)(uiControl *, HWND, NMHDR *, LRESULT *); + BOOL (*hscrollHandler)(uiControl *, HWND, WORD, LRESULT *); + uiControl *c; + + // just to ensure handlers[new HWND] initializes properly + // TODO gcc can't handle a struct keyword here? or is that a MSVC extension? + handler() + { + this->commandHandler = NULL; + this->notifyHandler = NULL; + this->hscrollHandler = NULL; + this->c = NULL; + } +}; + +static std::map<HWND, struct handler> handlers; + +void uiWindowsRegisterWM_COMMANDHandler(HWND hwnd, BOOL (*handler)(uiControl *, HWND, WORD, LRESULT *), uiControl *c) +{ + if (handlers[hwnd].commandHandler != NULL) + implbug("already registered a WM_COMMAND handler to window handle %p", hwnd); + handlers[hwnd].commandHandler = handler; + handlers[hwnd].c = c; +} + +void uiWindowsRegisterWM_NOTIFYHandler(HWND hwnd, BOOL (*handler)(uiControl *, HWND, NMHDR *, LRESULT *), uiControl *c) +{ + if (handlers[hwnd].notifyHandler != NULL) + implbug("already registered a WM_NOTIFY handler to window handle %p", hwnd); + handlers[hwnd].notifyHandler = handler; + handlers[hwnd].c = c; +} + +void uiWindowsRegisterWM_HSCROLLHandler(HWND hwnd, BOOL (*handler)(uiControl *, HWND, WORD, LRESULT *), uiControl *c) +{ + if (handlers[hwnd].hscrollHandler != NULL) + implbug("already registered a WM_HSCROLL handler to window handle %p", hwnd); + handlers[hwnd].hscrollHandler = handler; + handlers[hwnd].c = c; +} + +void uiWindowsUnregisterWM_COMMANDHandler(HWND hwnd) +{ + if (handlers[hwnd].commandHandler == NULL) + implbug("window handle %p not registered to receive WM_COMMAND events", hwnd); + handlers[hwnd].commandHandler = NULL; +} + +void uiWindowsUnregisterWM_NOTIFYHandler(HWND hwnd) +{ + if (handlers[hwnd].notifyHandler == NULL) + implbug("window handle %p not registered to receive WM_NOTIFY events", hwnd); + handlers[hwnd].notifyHandler = NULL; +} + +void uiWindowsUnregisterWM_HSCROLLHandler(HWND hwnd) +{ + if (handlers[hwnd].hscrollHandler == NULL) + implbug("window handle %p not registered to receive WM_HSCROLL events", hwnd); + handlers[hwnd].hscrollHandler = NULL; +} + +template<typename T> +static BOOL shouldRun(HWND hwnd, T method) +{ + // not from a window + if (hwnd == NULL) + return FALSE; + // don't bounce back if to the utility window, in which case act as if the message was ignored + if (IsChild(utilWindow, hwnd) != 0) + return FALSE; + // registered? + return method != NULL; +} + +BOOL runWM_COMMAND(WPARAM wParam, LPARAM lParam, LRESULT *lResult) +{ + HWND hwnd; + WORD arg3; + BOOL (*handler)(uiControl *, HWND, WORD, LRESULT *); + uiControl *c; + + hwnd = (HWND) lParam; + arg3 = HIWORD(wParam); + handler = handlers[hwnd].commandHandler; + c = handlers[hwnd].c; + if (shouldRun(hwnd, handler)) + return (*handler)(c, hwnd, arg3, lResult); + return FALSE; +} + +BOOL runWM_NOTIFY(WPARAM wParam, LPARAM lParam, LRESULT *lResult) +{ + HWND hwnd; + NMHDR *arg3; + BOOL (*handler)(uiControl *, HWND, NMHDR *, LRESULT *); + uiControl *c; + + arg3 = (NMHDR *) lParam; + hwnd = arg3->hwndFrom; + handler = handlers[hwnd].notifyHandler; + c = handlers[hwnd].c; + if (shouldRun(hwnd, handler)) + return (*handler)(c, hwnd, arg3, lResult); + return FALSE; +} + +BOOL runWM_HSCROLL(WPARAM wParam, LPARAM lParam, LRESULT *lResult) +{ + HWND hwnd; + WORD arg3; + BOOL (*handler)(uiControl *, HWND, WORD, LRESULT *); + uiControl *c; + + hwnd = (HWND) lParam; + arg3 = LOWORD(wParam); + handler = handlers[hwnd].hscrollHandler; + c = handlers[hwnd].c; + if (shouldRun(hwnd, handler)) + return (*handler)(c, hwnd, arg3, lResult); + return FALSE; +} + +static std::map<HWND, bool> wininichanges; + +void uiWindowsRegisterReceiveWM_WININICHANGE(HWND hwnd) +{ + if (wininichanges[hwnd]) + implbug("window handle %p already subscribed to receive WM_WINICHANGEs", hwnd); + wininichanges[hwnd] = true; +} + +void uiWindowsUnregisterReceiveWM_WININICHANGE(HWND hwnd) +{ + if (!wininichanges[hwnd]) + implbug("window handle %p not registered to receive WM_WININICHANGEs", hwnd); + wininichanges[hwnd] = false; +} + +void issueWM_WININICHANGE(WPARAM wParam, LPARAM lParam) +{ + struct wininichange *ch; + + for (const auto &iter : wininichanges) + SendMessageW(iter.first, WM_WININICHANGE, wParam, lParam); +} diff --git a/src/libui_sdl/libui/windows/fontbutton.cpp b/src/libui_sdl/libui/windows/fontbutton.cpp new file mode 100644 index 0000000..d2d4dab --- /dev/null +++ b/src/libui_sdl/libui/windows/fontbutton.cpp @@ -0,0 +1,122 @@ +// 14 april 2016 +#include "uipriv_windows.hpp" + +struct uiFontButton { + uiWindowsControl c; + HWND hwnd; + struct fontDialogParams params; + BOOL already; + void (*onChanged)(uiFontButton *, void *); + void *onChangedData; +}; + +static void uiFontButtonDestroy(uiControl *c) +{ + uiFontButton *b = uiFontButton(c); + + uiWindowsUnregisterWM_COMMANDHandler(b->hwnd); + destroyFontDialogParams(&(b->params)); + uiWindowsEnsureDestroyWindow(b->hwnd); + uiFreeControl(uiControl(b)); +} + +static void updateFontButtonLabel(uiFontButton *b) +{ + WCHAR *text; + + text = fontDialogParamsToString(&(b->params)); + setWindowText(b->hwnd, text); + uiFree(text); + + // changing the text might necessitate a change in the button's size + uiWindowsControlMinimumSizeChanged(uiWindowsControl(b)); +} + +static BOOL onWM_COMMAND(uiControl *c, HWND hwnd, WORD code, LRESULT *lResult) +{ + uiFontButton *b = uiFontButton(c); + HWND parent; + + if (code != BN_CLICKED) + return FALSE; + + parent = parentToplevel(b->hwnd); + if (showFontDialog(parent, &(b->params))) { + updateFontButtonLabel(b); + (*(b->onChanged))(b, b->onChangedData); + } + + *lResult = 0; + return TRUE; +} + +uiWindowsControlAllDefaultsExceptDestroy(uiFontButton) + +// from http://msdn.microsoft.com/en-us/library/windows/desktop/dn742486.aspx#sizingandspacing +#define buttonHeight 14 + +static void uiFontButtonMinimumSize(uiWindowsControl *c, int *width, int *height) +{ + uiFontButton *b = uiFontButton(c); + SIZE size; + uiWindowsSizing sizing; + int y; + + // try the comctl32 version 6 way + size.cx = 0; // explicitly ask for ideal size + size.cy = 0; + if (SendMessageW(b->hwnd, BCM_GETIDEALSIZE, 0, (LPARAM) (&size)) != FALSE) { + *width = size.cx; + *height = size.cy; + return; + } + + // that didn't work; fall back to using Microsoft's metrics + // Microsoft says to use a fixed width for all buttons; this isn't good enough + // use the text width instead, with some edge padding + *width = uiWindowsWindowTextWidth(b->hwnd) + (2 * GetSystemMetrics(SM_CXEDGE)); + y = buttonHeight; + uiWindowsGetSizing(b->hwnd, &sizing); + uiWindowsSizingDlgUnitsToPixels(&sizing, NULL, &y); + *height = y; +} + +static void defaultOnChanged(uiFontButton *b, void *data) +{ + // do nothing +} + +uiDrawTextFont *uiFontButtonFont(uiFontButton *b) +{ + // we don't own b->params.font; we have to add a reference + // we don't own b->params.familyName either; we have to copy it + return mkTextFont(b->params.font, TRUE, b->params.familyName, TRUE, b->params.size); +} + +void uiFontButtonOnChanged(uiFontButton *b, void (*f)(uiFontButton *, void *), void *data) +{ + b->onChanged = f; + b->onChangedData = data; +} + +uiFontButton *uiNewFontButton(void) +{ + uiFontButton *b; + + uiWindowsNewControl(uiFontButton, b); + + b->hwnd = uiWindowsEnsureCreateControlHWND(0, + L"button", L"you should not be seeing this", + BS_PUSHBUTTON | WS_TABSTOP, + hInstance, NULL, + TRUE); + + loadInitialFontDialogParams(&(b->params)); + + uiWindowsRegisterWM_COMMANDHandler(b->hwnd, onWM_COMMAND, uiControl(b)); + uiFontButtonOnChanged(b, defaultOnChanged, NULL); + + updateFontButtonLabel(b); + + return b; +} diff --git a/src/libui_sdl/libui/windows/fontdialog.cpp b/src/libui_sdl/libui/windows/fontdialog.cpp new file mode 100644 index 0000000..603a17d --- /dev/null +++ b/src/libui_sdl/libui/windows/fontdialog.cpp @@ -0,0 +1,686 @@ +// 14 april 2016 +#include "uipriv_windows.hpp" + +// TODOs +// - quote the Choose Font sample here for reference +// - the Choose Font sample defaults to Regular/Italic/Bold/Bold Italic in some case (no styles?); do we? find out what the case is +// - do we set initial family and style topmost as well? +// - this should probably just handle IDWriteFonts + +struct fontDialog { + HWND hwnd; + HWND familyCombobox; + HWND styleCombobox; + HWND sizeCombobox; + + struct fontDialogParams *params; + + fontCollection *fc; + + RECT sampleRect; + HWND sampleBox; + + // we store the current selections in case an invalid string is typed in (partial or nonexistent or invalid number) + // on OK, these are what are read + LRESULT curFamily; + LRESULT curStyle; + double curSize; + + // these are finding the style that's closest to the previous one (these fields) when changing a font + DWRITE_FONT_WEIGHT weight; + DWRITE_FONT_STYLE style; + DWRITE_FONT_STRETCH stretch; +}; + +static LRESULT cbAddString(HWND cb, const WCHAR *str) +{ + LRESULT lr; + + lr = SendMessageW(cb, CB_ADDSTRING, 0, (LPARAM) str); + if (lr == (LRESULT) CB_ERR || lr == (LRESULT) CB_ERRSPACE) + logLastError(L"error adding item to combobox"); + return lr; +} + +static LRESULT cbInsertString(HWND cb, const WCHAR *str, WPARAM pos) +{ + LRESULT lr; + + lr = SendMessageW(cb, CB_INSERTSTRING, pos, (LPARAM) str); + if (lr != (LRESULT) pos) + logLastError(L"error inserting item to combobox"); + return lr; +} + +static LRESULT cbGetItemData(HWND cb, WPARAM item) +{ + LRESULT data; + + data = SendMessageW(cb, CB_GETITEMDATA, item, 0); + if (data == (LRESULT) CB_ERR) + logLastError(L"error getting combobox item data for font dialog"); + return data; +} + +static void cbSetItemData(HWND cb, WPARAM item, LPARAM data) +{ + if (SendMessageW(cb, CB_SETITEMDATA, item, data) == (LRESULT) CB_ERR) + logLastError(L"error setting combobox item data"); +} + +static BOOL cbGetCurSel(HWND cb, LRESULT *sel) +{ + LRESULT n; + + n = SendMessageW(cb, CB_GETCURSEL, 0, 0); + if (n == (LRESULT) CB_ERR) + return FALSE; + if (sel != NULL) + *sel = n; + return TRUE; +} + +static void cbSetCurSel(HWND cb, WPARAM item) +{ + if (SendMessageW(cb, CB_SETCURSEL, item, 0) != (LRESULT) item) + logLastError(L"error selecting combobox item"); +} + +static LRESULT cbGetCount(HWND cb) +{ + LRESULT n; + + n = SendMessageW(cb, CB_GETCOUNT, 0, 0); + if (n == (LRESULT) CB_ERR) + logLastError(L"error getting combobox item count"); + return n; +} + +static void cbWipeAndReleaseData(HWND cb) +{ + IUnknown *obj; + LRESULT i, n; + + n = cbGetCount(cb); + for (i = 0; i < n; i++) { + obj = (IUnknown *) cbGetItemData(cb, (WPARAM) i); + obj->Release(); + } + SendMessageW(cb, CB_RESETCONTENT, 0, 0); +} + +static WCHAR *cbGetItemText(HWND cb, WPARAM item) +{ + LRESULT len; + WCHAR *text; + + // note: neither message includes the terminating L'\0' + len = SendMessageW(cb, CB_GETLBTEXTLEN, item, 0); + if (len == (LRESULT) CB_ERR) + logLastError(L"error getting item text length from combobox"); + text = (WCHAR *) uiAlloc((len + 1) * sizeof (WCHAR), "WCHAR[]"); + if (SendMessageW(cb, CB_GETLBTEXT, item, (LPARAM) text) != len) + logLastError(L"error getting item text from combobox"); + return text; +} + +static BOOL cbTypeToSelect(HWND cb, LRESULT *posOut, BOOL restoreAfter) +{ + WCHAR *text; + LRESULT pos; + DWORD selStart, selEnd; + + // start by saving the current selection as setting the item will change the selection + SendMessageW(cb, CB_GETEDITSEL, (WPARAM) (&selStart), (LPARAM) (&selEnd)); + text = windowText(cb); + pos = SendMessageW(cb, CB_FINDSTRINGEXACT, (WPARAM) (-1), (LPARAM) text); + if (pos == (LRESULT) CB_ERR) { + uiFree(text); + return FALSE; + } + cbSetCurSel(cb, (WPARAM) pos); + if (posOut != NULL) + *posOut = pos; + if (restoreAfter) + if (SendMessageW(cb, WM_SETTEXT, 0, (LPARAM) text) != (LRESULT) TRUE) + logLastError(L"error restoring old combobox text"); + uiFree(text); + // and restore the selection like above + // TODO isn't there a 32-bit version of this + if (SendMessageW(cb, CB_SETEDITSEL, 0, MAKELPARAM(selStart, selEnd)) != (LRESULT) TRUE) + logLastError(L"error restoring combobox edit selection"); + return TRUE; +} + +static void wipeStylesBox(struct fontDialog *f) +{ + cbWipeAndReleaseData(f->styleCombobox); +} + +static WCHAR *fontStyleName(struct fontCollection *fc, IDWriteFont *font) +{ + IDWriteLocalizedStrings *str; + WCHAR *wstr; + HRESULT hr; + + hr = font->GetFaceNames(&str); + if (hr != S_OK) + logHRESULT(L"error getting font style name for font dialog", hr); + wstr = fontCollectionCorrectString(fc, str); + str->Release(); + return wstr; +} + +static void queueRedrawSampleText(struct fontDialog *f) +{ + // TODO TRUE? + invalidateRect(f->sampleBox, NULL, TRUE); +} + +static void styleChanged(struct fontDialog *f) +{ + LRESULT pos; + BOOL selected; + IDWriteFont *font; + + selected = cbGetCurSel(f->styleCombobox, &pos); + if (!selected) // on deselect, do nothing + return; + f->curStyle = pos; + + font = (IDWriteFont *) cbGetItemData(f->styleCombobox, (WPARAM) (f->curStyle)); + // these are for the nearest match when changing the family; see below + f->weight = font->GetWeight(); + f->style = font->GetStyle(); + f->stretch = font->GetStretch(); + + queueRedrawSampleText(f); +} + +static void styleEdited(struct fontDialog *f) +{ + if (cbTypeToSelect(f->styleCombobox, &(f->curStyle), FALSE)) + styleChanged(f); +} + +static void familyChanged(struct fontDialog *f) +{ + LRESULT pos; + BOOL selected; + IDWriteFontFamily *family; + IDWriteFont *font, *matchFont; + DWRITE_FONT_WEIGHT weight; + DWRITE_FONT_STYLE style; + DWRITE_FONT_STRETCH stretch; + UINT32 i, n; + UINT32 matching; + WCHAR *label; + HRESULT hr; + + selected = cbGetCurSel(f->familyCombobox, &pos); + if (!selected) // on deselect, do nothing + return; + f->curFamily = pos; + + family = (IDWriteFontFamily *) cbGetItemData(f->familyCombobox, (WPARAM) (f->curFamily)); + + // for the nearest style match + // when we select a new family, we want the nearest style to the previously selected one to be chosen + // this is how the Choose Font sample does it + hr = family->GetFirstMatchingFont( + f->weight, + f->stretch, + f->style, + &matchFont); + if (hr != S_OK) + logHRESULT(L"error finding first matching font to previous style in font dialog", hr); + // we can't just compare pointers; a "newly created" object comes out + // the Choose Font sample appears to do this instead + weight = matchFont->GetWeight(); + style = matchFont->GetStyle(); + stretch = matchFont->GetStretch(); + matchFont->Release(); + + // TODO test mutliple streteches; all the fonts I have have only one stretch value? + wipeStylesBox(f); + n = family->GetFontCount(); + matching = 0; // a safe/suitable default just in case + for (i = 0; i < n; i++) { + hr = family->GetFont(i, &font); + if (hr != S_OK) + logHRESULT(L"error getting font for filling styles box", hr); + label = fontStyleName(f->fc, font); + pos = cbAddString(f->styleCombobox, label); + uiFree(label); + cbSetItemData(f->styleCombobox, (WPARAM) pos, (LPARAM) font); + if (font->GetWeight() == weight && + font->GetStyle() == style && + font->GetStretch() == stretch) + matching = i; + } + + // and now, load the match + cbSetCurSel(f->styleCombobox, (WPARAM) matching); + styleChanged(f); +} + +// TODO search language variants like the sample does +static void familyEdited(struct fontDialog *f) +{ + if (cbTypeToSelect(f->familyCombobox, &(f->curFamily), FALSE)) + familyChanged(f); +} + +static const struct { + const WCHAR *text; + double value; +} defaultSizes[] = { + { L"8", 8 }, + { L"9", 9 }, + { L"10", 10 }, + { L"11", 11 }, + { L"12", 12 }, + { L"14", 14 }, + { L"16", 16 }, + { L"18", 18 }, + { L"20", 20 }, + { L"22", 22 }, + { L"24", 24 }, + { L"26", 26 }, + { L"28", 28 }, + { L"36", 36 }, + { L"48", 48 }, + { L"72", 72 }, + { NULL, 0 }, +}; + +static void sizeChanged(struct fontDialog *f) +{ + LRESULT pos; + BOOL selected; + + selected = cbGetCurSel(f->sizeCombobox, &pos); + if (!selected) // on deselect, do nothing + return; + f->curSize = defaultSizes[pos].value; + queueRedrawSampleText(f); +} + +static void sizeEdited(struct fontDialog *f) +{ + WCHAR *wsize; + double size; + + // handle type-to-selection + if (cbTypeToSelect(f->sizeCombobox, NULL, FALSE)) { + sizeChanged(f); + return; + } + // selection not chosen, try to parse the typing + wsize = windowText(f->sizeCombobox); + // this is what the Choose Font dialog does; it swallows errors while the real ChooseFont() is not lenient (and only checks on OK) + size = wcstod(wsize, NULL); + if (size <= 0) // don't change on invalid size + return; + f->curSize = size; + queueRedrawSampleText(f); +} + +static void fontDialogDrawSampleText(struct fontDialog *f, ID2D1RenderTarget *rt) +{ + D2D1_COLOR_F color; + D2D1_BRUSH_PROPERTIES props; + ID2D1SolidColorBrush *black; + IDWriteFont *font; + IDWriteLocalizedStrings *sampleStrings; + BOOL exists; + WCHAR *sample; + WCHAR *family; + IDWriteTextFormat *format; + D2D1_RECT_F rect; + HRESULT hr; + + color.r = 0.0; + color.g = 0.0; + color.b = 0.0; + color.a = 1.0; + ZeroMemory(&props, sizeof (D2D1_BRUSH_PROPERTIES)); + props.opacity = 1.0; + // identity matrix + props.transform._11 = 1; + props.transform._22 = 1; + hr = rt->CreateSolidColorBrush( + &color, + &props, + &black); + if (hr != S_OK) + logHRESULT(L"error creating solid brush", hr); + + font = (IDWriteFont *) cbGetItemData(f->styleCombobox, (WPARAM) f->curStyle); + hr = font->GetInformationalStrings(DWRITE_INFORMATIONAL_STRING_SAMPLE_TEXT, &sampleStrings, &exists); + if (hr != S_OK) + exists = FALSE; + if (exists) { + sample = fontCollectionCorrectString(f->fc, sampleStrings); + sampleStrings->Release(); + } else + sample = L"The quick brown fox jumps over the lazy dog."; + + // DirectWrite doesn't allow creating a text format from a font; we need to get this ourselves + family = cbGetItemText(f->familyCombobox, f->curFamily); + hr = dwfactory->CreateTextFormat(family, + NULL, + font->GetWeight(), + font->GetStyle(), + font->GetStretch(), + // typographic points are 1/72 inch; this parameter is 1/96 inch + // fortunately Microsoft does this too, in https://msdn.microsoft.com/en-us/library/windows/desktop/dd371554%28v=vs.85%29.aspx + f->curSize * (96.0 / 72.0), + // see http://stackoverflow.com/questions/28397971/idwritefactorycreatetextformat-failing and https://msdn.microsoft.com/en-us/library/windows/desktop/dd368203.aspx + // TODO use the current locale again? + L"", + &format); + if (hr != S_OK) + logHRESULT(L"error creating IDWriteTextFormat", hr); + uiFree(family); + + rect.left = 0; + rect.top = 0; + rect.right = realGetSize(rt).width; + rect.bottom = realGetSize(rt).height; + rt->DrawText(sample, wcslen(sample), + format, + &rect, + black, + // TODO really? + D2D1_DRAW_TEXT_OPTIONS_NONE, + DWRITE_MEASURING_MODE_NATURAL); + + format->Release(); + if (exists) + uiFree(sample); + black->Release(); +} + +static LRESULT CALLBACK fontDialogSampleSubProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) +{ + ID2D1RenderTarget *rt; + struct fontDialog *f; + + switch (uMsg) { + case msgD2DScratchPaint: + rt = (ID2D1RenderTarget *) lParam; + f = (struct fontDialog *) dwRefData; + fontDialogDrawSampleText(f, rt); + return 0; + case WM_NCDESTROY: + if (RemoveWindowSubclass(hwnd, fontDialogSampleSubProc, uIdSubclass) == FALSE) + logLastError(L"error removing font dialog sample text subclass"); + break; + } + return DefSubclassProc(hwnd, uMsg, wParam, lParam); +} + +static void setupInitialFontDialogState(struct fontDialog *f) +{ + WCHAR wsize[512]; // this should be way more than enough + LRESULT pos; + + // first let's load the size + // the real font dialog: + // - if the chosen font size is in the list, it selects that item AND makes it topmost + // - if the chosen font size is not in the list, don't bother + // we'll simulate it by setting the text to a %f representation, then pretending as if it was entered + // TODO is 512 the correct number to pass to _snwprintf()? + // TODO will this revert to scientific notation? + _snwprintf(wsize, 512, L"%g", f->params->size); + // TODO make this a setWindowText() + if (SendMessageW(f->sizeCombobox, WM_SETTEXT, 0, (LPARAM) wsize) != (LRESULT) TRUE) + logLastError(L"error setting size combobox to initial font size"); + sizeEdited(f); + if (cbGetCurSel(f->sizeCombobox, &pos)) + if (SendMessageW(f->sizeCombobox, CB_SETTOPINDEX, (WPARAM) pos, 0) != 0) + logLastError(L"error making chosen size topmost in the size combobox"); + + // now we set the family and style + // we do this by first setting the previous style attributes, then simulating a font entered + f->weight = f->params->font->GetWeight(); + f->style = f->params->font->GetStyle(); + f->stretch = f->params->font->GetStretch(); + if (SendMessageW(f->familyCombobox, WM_SETTEXT, 0, (LPARAM) (f->params->familyName)) != (LRESULT) TRUE) + logLastError(L"error setting family combobox to initial font family"); + familyEdited(f); +} + +static struct fontDialog *beginFontDialog(HWND hwnd, LPARAM lParam) +{ + struct fontDialog *f; + UINT32 i, nFamilies; + IDWriteFontFamily *family; + WCHAR *wname; + LRESULT pos; + HWND samplePlacement; + HRESULT hr; + + f = uiNew(struct fontDialog); + f->hwnd = hwnd; + f->params = (struct fontDialogParams *) lParam; + + f->familyCombobox = getDlgItem(f->hwnd, rcFontFamilyCombobox); + f->styleCombobox = getDlgItem(f->hwnd, rcFontStyleCombobox); + f->sizeCombobox = getDlgItem(f->hwnd, rcFontSizeCombobox); + + f->fc = loadFontCollection(); + nFamilies = f->fc->fonts->GetFontFamilyCount(); + for (i = 0; i < nFamilies; i++) { + hr = f->fc->fonts->GetFontFamily(i, &family); + if (hr != S_OK) + logHRESULT(L"error getting font family", hr); + wname = fontCollectionFamilyName(f->fc, family); + pos = cbAddString(f->familyCombobox, wname); + uiFree(wname); + cbSetItemData(f->familyCombobox, (WPARAM) pos, (LPARAM) family); + } + + for (i = 0; defaultSizes[i].text != NULL; i++) + cbInsertString(f->sizeCombobox, defaultSizes[i].text, (WPARAM) i); + + samplePlacement = getDlgItem(f->hwnd, rcFontSamplePlacement); + uiWindowsEnsureGetWindowRect(samplePlacement, &(f->sampleRect)); + mapWindowRect(NULL, f->hwnd, &(f->sampleRect)); + uiWindowsEnsureDestroyWindow(samplePlacement); + f->sampleBox = newD2DScratch(f->hwnd, &(f->sampleRect), (HMENU) rcFontSamplePlacement, fontDialogSampleSubProc, (DWORD_PTR) f); + + setupInitialFontDialogState(f); + return f; +} + +static void endFontDialog(struct fontDialog *f, INT_PTR code) +{ + wipeStylesBox(f); + cbWipeAndReleaseData(f->familyCombobox); + fontCollectionFree(f->fc); + if (EndDialog(f->hwnd, code) == 0) + logLastError(L"error ending font dialog"); + uiFree(f); +} + +static INT_PTR tryFinishDialog(struct fontDialog *f, WPARAM wParam) +{ + IDWriteFontFamily *family; + + // cancelling + if (LOWORD(wParam) != IDOK) { + endFontDialog(f, 1); + return TRUE; + } + + // OK + destroyFontDialogParams(f->params); + f->params->font = (IDWriteFont *) cbGetItemData(f->styleCombobox, f->curStyle); + // we need to save font from being destroyed with the combobox + f->params->font->AddRef(); + f->params->size = f->curSize; + family = (IDWriteFontFamily *) cbGetItemData(f->familyCombobox, f->curFamily); + f->params->familyName = fontCollectionFamilyName(f->fc, family); + f->params->styleName = fontStyleName(f->fc, f->params->font); + endFontDialog(f, 2); + return TRUE; +} + +static INT_PTR CALLBACK fontDialogDlgProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + struct fontDialog *f; + + f = (struct fontDialog *) GetWindowLongPtrW(hwnd, DWLP_USER); + if (f == NULL) { + if (uMsg == WM_INITDIALOG) { + f = beginFontDialog(hwnd, lParam); + SetWindowLongPtrW(hwnd, DWLP_USER, (LONG_PTR) f); + return TRUE; + } + return FALSE; + } + + switch (uMsg) { + case WM_COMMAND: + SetWindowLongPtrW(f->hwnd, DWLP_MSGRESULT, 0); // just in case + switch (LOWORD(wParam)) { + case IDOK: + case IDCANCEL: + if (HIWORD(wParam) != BN_CLICKED) + return FALSE; + return tryFinishDialog(f, wParam); + case rcFontFamilyCombobox: + if (HIWORD(wParam) == CBN_SELCHANGE) { + familyChanged(f); + return TRUE; + } + if (HIWORD(wParam) == CBN_EDITCHANGE) { + familyEdited(f); + return TRUE; + } + return FALSE; + case rcFontStyleCombobox: + if (HIWORD(wParam) == CBN_SELCHANGE) { + styleChanged(f); + return TRUE; + } + if (HIWORD(wParam) == CBN_EDITCHANGE) { + styleEdited(f); + return TRUE; + } + return FALSE; + case rcFontSizeCombobox: + if (HIWORD(wParam) == CBN_SELCHANGE) { + sizeChanged(f); + return TRUE; + } + if (HIWORD(wParam) == CBN_EDITCHANGE) { + sizeEdited(f); + return TRUE; + } + return FALSE; + } + return FALSE; + } + return FALSE; +} + +BOOL showFontDialog(HWND parent, struct fontDialogParams *params) +{ + switch (DialogBoxParamW(hInstance, MAKEINTRESOURCE(rcFontDialog), parent, fontDialogDlgProc, (LPARAM) params)) { + case 1: // cancel + return FALSE; + case 2: // ok + // make the compiler happy by putting the return after the switch + break; + default: + logLastError(L"error running font dialog"); + } + return TRUE; +} + +static IDWriteFontFamily *tryFindFamily(IDWriteFontCollection *fc, const WCHAR *name) +{ + UINT32 index; + BOOL exists; + IDWriteFontFamily *family; + HRESULT hr; + + hr = fc->FindFamilyName(name, &index, &exists); + if (hr != S_OK) + logHRESULT(L"error finding font family for font dialog", hr); + if (!exists) + return NULL; + hr = fc->GetFontFamily(index, &family); + if (hr != S_OK) + logHRESULT(L"error extracting found font family for font dialog", hr); + return family; +} + +void loadInitialFontDialogParams(struct fontDialogParams *params) +{ + struct fontCollection *fc; + IDWriteFontFamily *family; + IDWriteFont *font; + HRESULT hr; + + // Our preferred font is Arial 10 Regular. + // 10 comes from the official font dialog. + // Arial Regular is a reasonable, if arbitrary, default; it's similar to the defaults on other systems. + // If Arial isn't found, we'll use Helvetica and then MS Sans Serif as fallbacks, and if not, we'll just grab the first font family in the collection. + + // We need the correct localized name for Regular (and possibly Arial too? let's say yes to be safe), so let's grab the strings from DirectWrite instead of hardcoding them. + fc = loadFontCollection(); + family = tryFindFamily(fc->fonts, L"Arial"); + if (family == NULL) { + family = tryFindFamily(fc->fonts, L"Helvetica"); + if (family == NULL) { + family = tryFindFamily(fc->fonts, L"MS Sans Serif"); + if (family == NULL) { + hr = fc->fonts->GetFontFamily(0, &family); + if (hr != S_OK) + logHRESULT(L"error getting first font out of font collection (worst case scenario)", hr); + } + } + } + + // next part is simple: just get the closest match to regular + hr = family->GetFirstMatchingFont( + DWRITE_FONT_WEIGHT_NORMAL, + DWRITE_FONT_STRETCH_NORMAL, + DWRITE_FONT_STYLE_NORMAL, + &font); + if (hr != S_OK) + logHRESULT(L"error getting Regular font from Arial", hr); + + params->font = font; + params->size = 10; + params->familyName = fontCollectionFamilyName(fc, family); + params->styleName = fontStyleName(fc, font); + + // don't release font; we still need it + family->Release(); + fontCollectionFree(fc); +} + +void destroyFontDialogParams(struct fontDialogParams *params) +{ + params->font->Release(); + uiFree(params->familyName); + uiFree(params->styleName); +} + +WCHAR *fontDialogParamsToString(struct fontDialogParams *params) +{ + WCHAR *text; + + // TODO dynamically allocate + text = (WCHAR *) uiAlloc(512 * sizeof (WCHAR), "WCHAR[]"); + _snwprintf(text, 512, L"%s %s %g", + params->familyName, + params->styleName, + params->size); + return text; +} diff --git a/src/libui_sdl/libui/windows/form.cpp b/src/libui_sdl/libui/windows/form.cpp new file mode 100644 index 0000000..febcc69 --- /dev/null +++ b/src/libui_sdl/libui/windows/form.cpp @@ -0,0 +1,319 @@ +// 8 june 2016 +#include "uipriv_windows.hpp" + +struct formChild { + uiControl *c; + HWND label; + int stretchy; + int height; +}; + +struct uiForm { + uiWindowsControl c; + HWND hwnd; + std::vector<struct formChild> *controls; + int padded; +}; + +static void formPadding(uiForm *f, int *xpadding, int *ypadding) +{ + uiWindowsSizing sizing; + + *xpadding = 0; + *ypadding = 0; + if (f->padded) { + uiWindowsGetSizing(f->hwnd, &sizing); + uiWindowsSizingStandardPadding(&sizing, xpadding, ypadding); + } +} + +// via http://msdn.microsoft.com/en-us/library/windows/desktop/dn742486.aspx#sizingandspacing +#define labelHeight 8 +#define labelYOffset 3 + +static void formRelayout(uiForm *f) +{ + RECT r; + int x, y, width, height; + int xpadding, ypadding; + int nStretchy; + int labelwid, stretchyht; + int thiswid; + int i; + int minimumWidth, minimumHeight; + uiWindowsSizing sizing; + int labelht, labelyoff; + int nVisible; + + if (f->controls->size() == 0) + return; + + uiWindowsEnsureGetClientRect(f->hwnd, &r); + x = r.left; + y = r.top; + width = r.right - r.left; + height = r.bottom - r.top; + + // 0) get this Form's padding + formPadding(f, &xpadding, &ypadding); + + // 1) get width of labels and height of non-stretchy controls + // this will tell us how much space will be left for controls + labelwid = 0; + stretchyht = height; + nStretchy = 0; + nVisible = 0; + for (struct formChild &fc : *(f->controls)) { + if (!uiControlVisible(fc.c)) { + ShowWindow(fc.label, SW_HIDE); + continue; + } + ShowWindow(fc.label, SW_SHOW); + nVisible++; + thiswid = uiWindowsWindowTextWidth(fc.label); + if (labelwid < thiswid) + labelwid = thiswid; + if (fc.stretchy) { + nStretchy++; + continue; + } + uiWindowsControlMinimumSize(uiWindowsControl(fc.c), &minimumWidth, &minimumHeight); + fc.height = minimumHeight; + stretchyht -= minimumHeight; + } + if (nVisible == 0) // nothing to do + return; + + // 2) inset the available rect by the needed padding + width -= xpadding; + height -= (nVisible - 1) * ypadding; + stretchyht -= (nVisible - 1) * ypadding; + + // 3) now get the width of controls and the height of stretchy controls + width -= labelwid; + if (nStretchy != 0) { + stretchyht /= nStretchy; + for (struct formChild &fc : *(f->controls)) { + if (!uiControlVisible(fc.c)) + continue; + if (fc.stretchy) + fc.height = stretchyht; + } + } + + // 4) get the y offset + labelyoff = labelYOffset; + uiWindowsGetSizing(f->hwnd, &sizing); + uiWindowsSizingDlgUnitsToPixels(&sizing, NULL, &labelyoff); + + // 5) now we can position controls + // first, make relative to the top-left corner of the container + // also prefer left alignment on Windows + x = labelwid + xpadding; + y = 0; + for (const struct formChild &fc : *(f->controls)) { + if (!uiControlVisible(fc.c)) + continue; + labelht = labelHeight; + uiWindowsGetSizing(f->hwnd, &sizing); + uiWindowsSizingDlgUnitsToPixels(&sizing, NULL, &labelht); + uiWindowsEnsureMoveWindowDuringResize(fc.label, 0, y + labelyoff - sizing.InternalLeading, labelwid, labelht); + uiWindowsEnsureMoveWindowDuringResize((HWND) uiControlHandle(fc.c), x, y, width, fc.height); + y += fc.height + ypadding; + } +} + +static void uiFormDestroy(uiControl *c) +{ + uiForm *f = uiForm(c); + + for (const struct formChild &fc : *(f->controls)) { + uiControlSetParent(fc.c, NULL); + uiControlDestroy(fc.c); + uiWindowsEnsureDestroyWindow(fc.label); + } + delete f->controls; + uiWindowsEnsureDestroyWindow(f->hwnd); + uiFreeControl(uiControl(f)); +} + +uiWindowsControlDefaultHandle(uiForm) +uiWindowsControlDefaultParent(uiForm) +uiWindowsControlDefaultSetParent(uiForm) +uiWindowsControlDefaultToplevel(uiForm) +uiWindowsControlDefaultVisible(uiForm) +uiWindowsControlDefaultShow(uiForm) +uiWindowsControlDefaultHide(uiForm) +uiWindowsControlDefaultEnabled(uiForm) +uiWindowsControlDefaultEnable(uiForm) +uiWindowsControlDefaultDisable(uiForm) + +static void uiFormSyncEnableState(uiWindowsControl *c, int enabled) +{ + uiForm *f = uiForm(c); + + if (uiWindowsShouldStopSyncEnableState(uiWindowsControl(f), enabled)) + return; + for (const struct formChild &fc : *(f->controls)) + uiWindowsControlSyncEnableState(uiWindowsControl(fc.c), enabled); +} + +uiWindowsControlDefaultSetParentHWND(uiForm) + +static void uiFormMinimumSize(uiWindowsControl *c, int *width, int *height) +{ + uiForm *f = uiForm(c); + int xpadding, ypadding; + int nStretchy; + // these two contain the largest minimum width and height of all stretchy controls in the form + // all stretchy controls will use this value to determine the final minimum size + int maxLabelWidth, maxControlWidth; + int maxStretchyHeight; + int labelwid; + int i; + int minimumWidth, minimumHeight; + int nVisible; + uiWindowsSizing sizing; + + *width = 0; + *height = 0; + if (f->controls->size() == 0) + return; + + // 0) get this Form's padding + formPadding(f, &xpadding, &ypadding); + + // 1) determine the longest width of all controls and labels; add in the height of non-stretchy controls and get (but not add in) the largest heights of stretchy controls + // we still add in like direction of stretchy controls + nStretchy = 0; + maxLabelWidth = 0; + maxControlWidth = 0; + maxStretchyHeight = 0; + nVisible = 0; + for (const struct formChild &fc : *(f->controls)) { + if (!uiControlVisible(fc.c)) + continue; + nVisible++; + labelwid = uiWindowsWindowTextWidth(fc.label); + if (maxLabelWidth < labelwid) + maxLabelWidth = labelwid; + uiWindowsControlMinimumSize(uiWindowsControl(fc.c), &minimumWidth, &minimumHeight); + if (fc.stretchy) { + nStretchy++; + if (maxStretchyHeight < minimumHeight) + maxStretchyHeight = minimumHeight; + } + if (maxControlWidth < minimumWidth) + maxControlWidth = minimumWidth; + if (!fc.stretchy) + *height += minimumHeight; + } + if (nVisible == 0) // nothing to show; return 0x0 + return; + *width += maxLabelWidth + maxControlWidth; + + // 2) outset the desired rect with the needed padding + *width += xpadding; + *height += (nVisible - 1) * ypadding; + + // 3) and now we can add in stretchy controls + *height += nStretchy * maxStretchyHeight; +} + +static void uiFormMinimumSizeChanged(uiWindowsControl *c) +{ + uiForm *f = uiForm(c); + + if (uiWindowsControlTooSmall(uiWindowsControl(f))) { + uiWindowsControlContinueMinimumSizeChanged(uiWindowsControl(f)); + return; + } + formRelayout(f); +} + +uiWindowsControlDefaultLayoutRect(uiForm) +uiWindowsControlDefaultAssignControlIDZOrder(uiForm) + +static void uiFormChildVisibilityChanged(uiWindowsControl *c) +{ + // TODO eliminate the redundancy + uiWindowsControlMinimumSizeChanged(c); +} + +static void formArrangeChildren(uiForm *f) +{ + LONG_PTR controlID; + HWND insertAfter; + int i; + + controlID = 100; + insertAfter = NULL; + for (const struct formChild &fc : *(f->controls)) { + // TODO assign label ID and z-order + uiWindowsControlAssignControlIDZOrder(uiWindowsControl(fc.c), &controlID, &insertAfter); + } +} + +void uiFormAppend(uiForm *f, const char *label, uiControl *c, int stretchy) +{ + struct formChild fc; + WCHAR *wlabel; + + fc.c = c; + wlabel = toUTF16(label); + fc.label = uiWindowsEnsureCreateControlHWND(0, + L"STATIC", wlabel, + SS_LEFT | SS_NOPREFIX, + hInstance, NULL, + TRUE); + uiFree(wlabel); + uiWindowsEnsureSetParentHWND(fc.label, f->hwnd); + fc.stretchy = stretchy; + uiControlSetParent(fc.c, uiControl(f)); + uiWindowsControlSetParentHWND(uiWindowsControl(fc.c), f->hwnd); + f->controls->push_back(fc); + formArrangeChildren(f); + uiWindowsControlMinimumSizeChanged(uiWindowsControl(f)); +} + +void uiFormDelete(uiForm *f, int index) +{ + struct formChild fc; + + fc = (*(f->controls))[index]; + uiControlSetParent(fc.c, NULL); + uiWindowsControlSetParentHWND(uiWindowsControl(fc.c), NULL); + uiWindowsEnsureDestroyWindow(fc.label); + f->controls->erase(f->controls->begin() + index); + formArrangeChildren(f); + uiWindowsControlMinimumSizeChanged(uiWindowsControl(f)); +} + +int uiFormPadded(uiForm *f) +{ + return f->padded; +} + +void uiFormSetPadded(uiForm *f, int padded) +{ + f->padded = padded; + uiWindowsControlMinimumSizeChanged(uiWindowsControl(f)); +} + +static void onResize(uiWindowsControl *c) +{ + formRelayout(uiForm(c)); +} + +uiForm *uiNewForm(void) +{ + uiForm *f; + + uiWindowsNewControl(uiForm, f); + + f->hwnd = uiWindowsMakeContainer(uiWindowsControl(f), onResize); + + f->controls = new std::vector<struct formChild>; + + return f; +} diff --git a/src/libui_sdl/libui/windows/graphemes.cpp b/src/libui_sdl/libui/windows/graphemes.cpp new file mode 100644 index 0000000..355e403 --- /dev/null +++ b/src/libui_sdl/libui/windows/graphemes.cpp @@ -0,0 +1,80 @@ +// 25 may 2016 +#include "uipriv_windows.hpp" + +// We could use CharNext() to generate grapheme cluster boundaries, but it doesn't handle surrogate pairs properly (see http://archives.miloush.net/michkap/archive/2008/12/16/9223301.html). +// So let's use Uniscribe (see http://archives.miloush.net/michkap/archive/2005/01/14/352802.html) +// See also http://www.catch22.net/tuts/uniscribe-mysteries and http://www.catch22.net/tuts/keyboard-navigation for more details. + +static HRESULT itemize(WCHAR *msg, size_t len, SCRIPT_ITEM **out, int *outn) +{ + SCRIPT_CONTROL sc; + SCRIPT_STATE ss; + SCRIPT_ITEM *items; + size_t maxItems; + int n; + HRESULT hr; + + // make sure these are zero-initialized to avoid mangling the text + ZeroMemory(&sc, sizeof (SCRIPT_CONTROL)); + ZeroMemory(&ss, sizeof (SCRIPT_STATE)); + + maxItems = len + 2; + for (;;) { + items = new SCRIPT_ITEM[maxItems]; + hr = ScriptItemize(msg, len, + maxItems, + &sc, &ss, + items, &n); + if (hr == S_OK) + break; + // otherwise either an error or not enough room + delete[] items; + if (hr != E_OUTOFMEMORY) + return hr; + maxItems *= 2; // add some more and try again + } + + *out = items; + *outn = n; + return S_OK; +} + +size_t *graphemes(WCHAR *msg) +{ + size_t len; + SCRIPT_ITEM *items; + int i, n; + size_t *out; + size_t *op; + SCRIPT_LOGATTR *logattr; + int j, nn; + HRESULT hr; + + len = wcslen(msg); + hr = itemize(msg, len, &items, &n); + if (hr != S_OK) + logHRESULT(L"error itemizing string for finding grapheme cluster boundaries", hr); + + // should be enough; 2 more just to be safe + out = (size_t *) uiAlloc((len + 2) * sizeof (size_t), "size_t[]"); + op = out; + + // note that there are actually n + 1 elements in items + for (i = 0; i < n; i++) { + nn = items[i + 1].iCharPos - items[i].iCharPos; + logattr = new SCRIPT_LOGATTR[nn]; + hr = ScriptBreak(msg + items[i].iCharPos, nn, + &(items[i].a), logattr); + if (hr != S_OK) + logHRESULT(L"error breaking string for finding grapheme cluster boundaries", hr); + for (j = 0; j < nn; j++) + if (logattr[j].fCharStop != 0) + *op++ = items[i].iCharPos + j; + delete[] logattr; + } + // and handle the last item for the end of the string + *op++ = items[i].iCharPos; + + delete[] items; + return out; +} diff --git a/src/libui_sdl/libui/windows/grid.cpp b/src/libui_sdl/libui/windows/grid.cpp new file mode 100644 index 0000000..c63cd1e --- /dev/null +++ b/src/libui_sdl/libui/windows/grid.cpp @@ -0,0 +1,658 @@ +// 10 june 2016 +#include "uipriv_windows.hpp" + +// TODO compare with GTK+: +// - what happens if you call InsertAt() twice? +// - what happens if you call Append() twice? + +// TODOs +// - the Assorted page has clipping and repositioning issues + +struct gridChild { + uiControl *c; + int left; + int top; + int xspan; + int yspan; + int hexpand; + uiAlign halign; + int vexpand; + uiAlign valign; + + // have these here so they don't need to be reallocated each relayout + int finalx, finaly; + int finalwidth, finalheight; + int minwidth, minheight; +}; + +struct uiGrid { + uiWindowsControl c; + HWND hwnd; + std::vector<struct gridChild *> *children; + std::map<uiControl *, size_t> *indexof; + int padded; + + int xmin, ymin; + int xmax, ymax; +}; + +static bool gridRecomputeMinMax(uiGrid *g) +{ + bool first = true; + + for (struct gridChild *gc : *(g->children)) { + // this is important; we want g->xmin/g->ymin to satisfy gridLayoutData::visibleRow()/visibleColumn() + if (!uiControlVisible(gc->c)) + continue; + if (first) { + g->xmin = gc->left; + g->ymin = gc->top; + g->xmax = gc->left + gc->xspan; + g->ymax = gc->top + gc->yspan; + first = false; + continue; + } + if (g->xmin > gc->left) + g->xmin = gc->left; + if (g->ymin > gc->top) + g->ymin = gc->top; + if (g->xmax < (gc->left + gc->xspan)) + g->xmax = gc->left + gc->xspan; + if (g->ymax < (gc->top + gc->yspan)) + g->ymax = gc->top + gc->yspan; + } + return first != false; +} + +#define xcount(g) ((g)->xmax - (g)->xmin) +#define ycount(g) ((g)->ymax - (g)->ymin) +#define toxindex(g, x) ((x) - (g)->xmin) +#define toyindex(g, y) ((y) - (g)->ymin) + +class gridLayoutData { + int ycount; +public: + int **gg; // topological map gg[y][x] = control index + int *colwidths; + int *rowheights; + bool *hexpand; + bool *vexpand; + int nVisibleRows; + int nVisibleColumns; + + bool noVisible; + + gridLayoutData(uiGrid *g) + { + size_t i; + int x, y; + + this->noVisible = gridRecomputeMinMax(g); + + this->gg = new int *[ycount(g)]; + for (y = 0; y < ycount(g); y++) { + this->gg[y] = new int[xcount(g)]; + for (x = 0; x < xcount(g); x++) + this->gg[y][x] = -1; + } + + for (i = 0; i < g->children->size(); i++) { + struct gridChild *gc; + + gc = (*(g->children))[i]; + if (!uiControlVisible(gc->c)) + continue; + for (y = gc->top; y < gc->top + gc->yspan; y++) + for (x = gc->left; x < gc->left + gc->xspan; x++) + this->gg[toyindex(g, y)][toxindex(g, x)] = i; + } + + this->colwidths = new int[xcount(g)]; + ZeroMemory(this->colwidths, xcount(g) * sizeof (int)); + this->rowheights = new int[ycount(g)]; + ZeroMemory(this->rowheights, ycount(g) * sizeof (int)); + this->hexpand = new bool[xcount(g)]; + ZeroMemory(this->hexpand, xcount(g) * sizeof (bool)); + this->vexpand = new bool[ycount(g)]; + ZeroMemory(this->vexpand, ycount(g) * sizeof (bool)); + + this->ycount = ycount(g); + + // if a row or column only contains emptys and spanning cells of a opposite-direction spannings, it is invisible and should not be considered for padding amount calculations + // note that the first row and column will always be visible because gridRecomputeMinMax() computed a smallest fitting rectangle + if (this->noVisible) + return; + this->nVisibleRows = 0; + for (y = 0; y < this->ycount; y++) + if (this->visibleRow(g, y)) + this->nVisibleRows++; + this->nVisibleColumns = 0; + for (x = 0; x < xcount(g); x++) + if (this->visibleColumn(g, x)) + this->nVisibleColumns++; + } + + ~gridLayoutData() + { + size_t y; + + delete[] this->hexpand; + delete[] this->vexpand; + delete[] this->colwidths; + delete[] this->rowheights; + for (y = 0; y < this->ycount; y++) + delete[] this->gg[y]; + delete[] this->gg; + } + + bool visibleRow(uiGrid *g, int y) + { + int x; + struct gridChild *gc; + + for (x = 0; x < xcount(g); x++) + if (this->gg[y][x] != -1) { + gc = (*(g->children))[this->gg[y][x]]; + if (gc->yspan == 1 || gc->top - g->ymin == y) + return true; + } + return false; + } + + bool visibleColumn(uiGrid *g, int x) + { + int y; + struct gridChild *gc; + + for (y = 0; y < this->ycount; y++) + if (this->gg[y][x] != -1) { + gc = (*(g->children))[this->gg[y][x]]; + if (gc->xspan == 1 || gc->left - g->xmin == x) + return true; + } + return false; + } +}; + +static void gridPadding(uiGrid *g, int *xpadding, int *ypadding) +{ + uiWindowsSizing sizing; + + *xpadding = 0; + *ypadding = 0; + if (g->padded) { + uiWindowsGetSizing(g->hwnd, &sizing); + uiWindowsSizingStandardPadding(&sizing, xpadding, ypadding); + } +} + +static void gridRelayout(uiGrid *g) +{ + RECT r; + int x, y, width, height; + gridLayoutData *ld; + int xpadding, ypadding; + int ix, iy; + int iwidth, iheight; + int i; + struct gridChild *gc; + int nhexpand, nvexpand; + + if (g->children->size() == 0) + return; // nothing to do + + uiWindowsEnsureGetClientRect(g->hwnd, &r); + x = r.left; + y = r.top; + width = r.right - r.left; + height = r.bottom - r.top; + + gridPadding(g, &xpadding, &ypadding); + ld = new gridLayoutData(g); + if (ld->noVisible) { // nothing to do + delete ld; + return; + } + + // 0) discount padding from width/height + width -= (ld->nVisibleColumns - 1) * xpadding; + height -= (ld->nVisibleRows - 1) * ypadding; + + // 1) compute colwidths and rowheights before handling expansion + // we only count non-spanning controls to avoid weirdness + for (iy = 0; iy < ycount(g); iy++) + for (ix = 0; ix < xcount(g); ix++) { + i = ld->gg[iy][ix]; + if (i == -1) + continue; + gc = (*(g->children))[i]; + uiWindowsControlMinimumSize(uiWindowsControl(gc->c), &iwidth, &iheight); + if (gc->xspan == 1) + if (ld->colwidths[ix] < iwidth) + ld->colwidths[ix] = iwidth; + if (gc->yspan == 1) + if (ld->rowheights[iy] < iheight) + ld->rowheights[iy] = iheight; + // save these for step 6 + gc->minwidth = iwidth; + gc->minheight = iheight; + } + + // 2) figure out which rows/columns expand but not span + // we need to know which expanding rows/columns don't span before we can handle the ones that do + for (i = 0; i < g->children->size(); i++) { + gc = (*(g->children))[i]; + if (!uiControlVisible(gc->c)) + continue; + if (gc->hexpand && gc->xspan == 1) + ld->hexpand[toxindex(g, gc->left)] = true; + if (gc->vexpand && gc->yspan == 1) + ld->vexpand[toyindex(g, gc->top)] = true; + } + + // 3) figure out which rows/columns expand that do span + // the way we handle this is simple: if none of the spanned rows/columns expand, make all rows/columns expand + for (i = 0; i < g->children->size(); i++) { + gc = (*(g->children))[i]; + if (!uiControlVisible(gc->c)) + continue; + if (gc->hexpand && gc->xspan != 1) { + bool doit = true; + + for (ix = gc->left; ix < gc->left + gc->xspan; ix++) + if (ld->hexpand[toxindex(g, ix)]) { + doit = false; + break; + } + if (doit) + for (ix = gc->left; ix < gc->left + gc->xspan; ix++) + ld->hexpand[toxindex(g, ix)] = true; + } + if (gc->vexpand && gc->yspan != 1) { + bool doit = true; + + for (iy = gc->top; iy < gc->top + gc->yspan; iy++) + if (ld->vexpand[toyindex(g, iy)]) { + doit = false; + break; + } + if (doit) + for (iy = gc->top; iy < gc->top + gc->yspan; iy++) + ld->vexpand[toyindex(g, iy)] = true; + } + } + + // 4) compute and assign expanded widths/heights + nhexpand = 0; + nvexpand = 0; + for (i = 0; i < xcount(g); i++) + if (ld->hexpand[i]) + nhexpand++; + else + width -= ld->colwidths[i]; + for (i = 0; i < ycount(g); i++) + if (ld->vexpand[i]) + nvexpand++; + else + height -= ld->rowheights[i]; + for (i = 0; i < xcount(g); i++) + if (ld->hexpand[i]) + ld->colwidths[i] = width / nhexpand; + for (i = 0; i < ycount(g); i++) + if (ld->vexpand[i]) + ld->rowheights[i] = height / nvexpand; + + // 5) reset the final coordinates for the next step + for (i = 0; i < g->children->size(); i++) { + gc = (*(g->children))[i]; + if (!uiControlVisible(gc->c)) + continue; + gc->finalx = 0; + gc->finaly = 0; + gc->finalwidth = 0; + gc->finalheight = 0; + } + + // 6) compute cell positions and sizes + for (iy = 0; iy < ycount(g); iy++) { + int curx; + int prev; + + curx = 0; + prev = -1; + for (ix = 0; ix < xcount(g); ix++) { + if (!ld->visibleColumn(g, ix)) + continue; + i = ld->gg[iy][ix]; + if (i != -1) { + gc = (*(g->children))[i]; + if (iy == toyindex(g, gc->top)) { // don't repeat this step if the control spans vertically + if (i != prev) + gc->finalx = curx; + else + gc->finalwidth += xpadding; + gc->finalwidth += ld->colwidths[ix]; + } + } + curx += ld->colwidths[ix] + xpadding; + prev = i; + } + } + for (ix = 0; ix < xcount(g); ix++) { + int cury; + int prev; + + cury = 0; + prev = -1; + for (iy = 0; iy < ycount(g); iy++) { + if (!ld->visibleRow(g, iy)) + continue; + i = ld->gg[iy][ix]; + if (i != -1) { + gc = (*(g->children))[i]; + if (ix == toxindex(g, gc->left)) { // don't repeat this step if the control spans horizontally + if (i != prev) + gc->finaly = cury; + else + gc->finalheight += ypadding; + gc->finalheight += ld->rowheights[iy]; + } + } + cury += ld->rowheights[iy] + ypadding; + prev = i; + } + } + + // 7) everything as it stands now is set for xalign == Fill yalign == Fill; set the correct alignments + // this is why we saved minwidth/minheight above + for (i = 0; i < g->children->size(); i++) { + gc = (*(g->children))[i]; + if (!uiControlVisible(gc->c)) + continue; + if (gc->halign != uiAlignFill) { + switch (gc->halign) { + case uiAlignEnd: + gc->finalx += gc->finalwidth - gc->minwidth; + break; + case uiAlignCenter: + gc->finalx += (gc->finalwidth - gc->minwidth) / 2; + break; + } + gc->finalwidth = gc->minwidth; // for all three + } + if (gc->valign != uiAlignFill) { + switch (gc->valign) { + case uiAlignEnd: + gc->finaly += gc->finalheight - gc->minheight; + break; + case uiAlignCenter: + gc->finaly += (gc->finalheight - gc->minheight) / 2; + break; + } + gc->finalheight = gc->minheight; // for all three + } + } + + // 8) and FINALLY we resize + for (iy = 0; iy < ycount(g); iy++) + for (ix = 0; ix < xcount(g); ix++) { + i = ld->gg[iy][ix]; + if (i != -1) { // treat empty cells like spaces + gc = (*(g->children))[i]; + uiWindowsEnsureMoveWindowDuringResize( + (HWND) uiControlHandle(gc->c), + gc->finalx,//TODO + x, + gc->finaly,//TODO + y, + gc->finalwidth, + gc->finalheight); + } + } + + delete ld; +} + +static void uiGridDestroy(uiControl *c) +{ + uiGrid *g = uiGrid(c); + + for (struct gridChild *gc : *(g->children)) { + uiControlSetParent(gc->c, NULL); + uiControlDestroy(gc->c); + uiFree(gc); + } + delete g->indexof; + delete g->children; + uiWindowsEnsureDestroyWindow(g->hwnd); + uiFreeControl(uiControl(g)); +} + +uiWindowsControlDefaultHandle(uiGrid) +uiWindowsControlDefaultParent(uiGrid) +uiWindowsControlDefaultSetParent(uiGrid) +uiWindowsControlDefaultToplevel(uiGrid) +uiWindowsControlDefaultVisible(uiGrid) +uiWindowsControlDefaultShow(uiGrid) +uiWindowsControlDefaultHide(uiGrid) +uiWindowsControlDefaultEnabled(uiGrid) +uiWindowsControlDefaultEnable(uiGrid) +uiWindowsControlDefaultDisable(uiGrid) + +static void uiGridSyncEnableState(uiWindowsControl *c, int enabled) +{ + uiGrid *g = uiGrid(c); + + if (uiWindowsShouldStopSyncEnableState(uiWindowsControl(g), enabled)) + return; + for (const struct gridChild *gc : *(g->children)) + uiWindowsControlSyncEnableState(uiWindowsControl(gc->c), enabled); +} + +uiWindowsControlDefaultSetParentHWND(uiGrid) + +static void uiGridMinimumSize(uiWindowsControl *c, int *width, int *height) +{ + uiGrid *g = uiGrid(c); + int xpadding, ypadding; + gridLayoutData *ld; + int x, y; + int i; + struct gridChild *gc; + int minwid, minht; + int colwidth, rowheight; + + *width = 0; + *height = 0; + if (g->children->size() == 0) + return; // nothing to do + + gridPadding(g, &xpadding, &ypadding); + ld = new gridLayoutData(g); + if (ld->noVisible) { // nothing to do; return 0x0 + delete ld; + return; + } + + // 1) compute colwidths and rowheights before handling expansion + // TODO put this in its own function (but careful about the spanning calculation in gridRelayout()) + for (y = 0; y < ycount(g); y++) + for (x = 0; x < xcount(g); x++) { + i = ld->gg[y][x]; + if (i == -1) + continue; + gc = (*(g->children))[i]; + uiWindowsControlMinimumSize(uiWindowsControl(gc->c), &minwid, &minht); + // allot equal space in the presence of spanning to keep things sane + if (ld->colwidths[x] < minwid / gc->xspan) + ld->colwidths[x] = minwid / gc->xspan; + if (ld->rowheights[y] < minht / gc->yspan) + ld->rowheights[y] = minht / gc->yspan; + // save these for step 6 + gc->minwidth = minwid; + gc->minheight = minht; + } + + // 2) compute total column width/row height + colwidth = 0; + rowheight = 0; + for (x = 0; x < xcount(g); x++) + colwidth += ld->colwidths[x]; + for (y = 0; y < ycount(g); y++) + rowheight += ld->rowheights[y]; + + // and that's it; just account for padding + *width = colwidth + (ld->nVisibleColumns - 1) * xpadding; + *height = rowheight + (ld->nVisibleRows - 1) * ypadding; +} + +static void uiGridMinimumSizeChanged(uiWindowsControl *c) +{ + uiGrid *g = uiGrid(c); + + if (uiWindowsControlTooSmall(uiWindowsControl(g))) { + uiWindowsControlContinueMinimumSizeChanged(uiWindowsControl(g)); + return; + } + gridRelayout(g); +} + +uiWindowsControlDefaultLayoutRect(uiGrid) +uiWindowsControlDefaultAssignControlIDZOrder(uiGrid) + +static void uiGridChildVisibilityChanged(uiWindowsControl *c) +{ + // TODO eliminate the redundancy + uiWindowsControlMinimumSizeChanged(c); +} + +// must have called gridRecomputeMinMax() first +static void gridArrangeChildren(uiGrid *g) +{ + LONG_PTR controlID; + HWND insertAfter; + gridLayoutData *ld; + bool *visited; + int x, y; + int i; + struct gridChild *gc; + + if (g->children->size() == 0) + return; // nothing to do + ld = new gridLayoutData(g); + controlID = 100; + insertAfter = NULL; + visited = new bool[g->children->size()]; + ZeroMemory(visited, g->children->size() * sizeof (bool)); + for (y = 0; y < ycount(g); y++) + for (x = 0; x < xcount(g); x++) { + i = ld->gg[y][x]; + if (i == -1) + continue; + if (visited[i]) + continue; + visited[i] = true; + gc = (*(g->children))[i]; + uiWindowsControlAssignControlIDZOrder(uiWindowsControl(gc->c), &controlID, &insertAfter); + } + delete[] visited; + delete ld; +} + +static struct gridChild *toChild(uiControl *c, int xspan, int yspan, int hexpand, uiAlign halign, int vexpand, uiAlign valign) +{ + struct gridChild *gc; + + if (xspan < 0) + userbug("You cannot have a negative xspan in a uiGrid cell."); + if (yspan < 0) + userbug("You cannot have a negative yspan in a uiGrid cell."); + gc = uiNew(struct gridChild); + gc->c = c; + gc->xspan = xspan; + gc->yspan = yspan; + gc->hexpand = hexpand; + gc->halign = halign; + gc->vexpand = vexpand; + gc->valign = valign; + return gc; +} + +static void add(uiGrid *g, struct gridChild *gc) +{ + uiControlSetParent(gc->c, uiControl(g)); + uiWindowsControlSetParentHWND(uiWindowsControl(gc->c), g->hwnd); + g->children->push_back(gc); + (*(g->indexof))[gc->c] = g->children->size() - 1; + gridRecomputeMinMax(g); + gridArrangeChildren(g); + uiWindowsControlMinimumSizeChanged(uiWindowsControl(g)); +} + +void uiGridAppend(uiGrid *g, uiControl *c, int left, int top, int xspan, int yspan, int hexpand, uiAlign halign, int vexpand, uiAlign valign) +{ + struct gridChild *gc; + + gc = toChild(c, xspan, yspan, hexpand, halign, vexpand, valign); + gc->left = left; + gc->top = top; + add(g, gc); +} + +// TODO decide what happens if existing is NULL +void uiGridInsertAt(uiGrid *g, uiControl *c, uiControl *existing, uiAt at, int xspan, int yspan, int hexpand, uiAlign halign, int vexpand, uiAlign valign) +{ + struct gridChild *gc; + struct gridChild *other; + + gc = toChild(c, xspan, yspan, hexpand, halign, vexpand, valign); + other = (*(g->children))[(*(g->indexof))[existing]]; + switch (at) { + case uiAtLeading: + gc->left = other->left - gc->xspan; + gc->top = other->top; + break; + case uiAtTop: + gc->left = other->left; + gc->top = other->top - gc->yspan; + break; + case uiAtTrailing: + gc->left = other->left + other->xspan; + gc->top = other->top; + break; + case uiAtBottom: + gc->left = other->left; + gc->top = other->top + other->yspan; + break; + // TODO add error checks to ALL enums + } + add(g, gc); +} + +int uiGridPadded(uiGrid *g) +{ + return g->padded; +} + +void uiGridSetPadded(uiGrid *g, int padded) +{ + g->padded = padded; + uiWindowsControlMinimumSizeChanged(uiWindowsControl(g)); +} + +static void onResize(uiWindowsControl *c) +{ + gridRelayout(uiGrid(c)); +} + +uiGrid *uiNewGrid(void) +{ + uiGrid *g; + + uiWindowsNewControl(uiGrid, g); + + g->hwnd = uiWindowsMakeContainer(uiWindowsControl(g), onResize); + + g->children = new std::vector<struct gridChild *>; + g->indexof = new std::map<uiControl *, size_t>; + + return g; +} diff --git a/src/libui_sdl/libui/windows/group.cpp b/src/libui_sdl/libui/windows/group.cpp new file mode 100644 index 0000000..8824c5a --- /dev/null +++ b/src/libui_sdl/libui/windows/group.cpp @@ -0,0 +1,217 @@ +// 16 may 2015 +#include "uipriv_windows.hpp" + +struct uiGroup { + uiWindowsControl c; + HWND hwnd; + struct uiControl *child; + int margined; +}; + +// from https://msdn.microsoft.com/en-us/library/windows/desktop/dn742486.aspx#sizingandspacing +#define groupXMargin 6 +#define groupYMarginTop 11 /* note this value /includes/ the groupbox label */ +#define groupYMarginBottom 7 + +// unfortunately because the client area of a groupbox includes the frame and caption text, we have to apply some margins ourselves, even if we don't want "any" +// these were deduced by hand based on the standard DLU conversions; the X and Y top margins are the width and height, respectively, of one character cell +// they can be fine-tuned later +#define groupUnmarginedXMargin 4 +#define groupUnmarginedYMarginTop 8 +#define groupUnmarginedYMarginBottom 3 + +static void groupMargins(uiGroup *g, int *mx, int *mtop, int *mbottom) +{ + uiWindowsSizing sizing; + + *mx = groupUnmarginedXMargin; + *mtop = groupUnmarginedYMarginTop; + *mbottom = groupUnmarginedYMarginBottom; + if (g->margined) { + *mx = groupXMargin; + *mtop = groupYMarginTop; + *mbottom = groupYMarginBottom; + } + uiWindowsGetSizing(g->hwnd, &sizing); + uiWindowsSizingDlgUnitsToPixels(&sizing, mx, mtop); + uiWindowsSizingDlgUnitsToPixels(&sizing, NULL, mbottom); +} + +static void groupRelayout(uiGroup *g) +{ + RECT r; + int mx, mtop, mbottom; + + if (g->child == NULL) + return; + uiWindowsEnsureGetClientRect(g->hwnd, &r); + groupMargins(g, &mx, &mtop, &mbottom); + r.left += mx; + r.top += mtop; + r.right -= mx; + r.bottom -= mbottom; + uiWindowsEnsureMoveWindowDuringResize((HWND) uiControlHandle(g->child), r.left, r.top, r.right - r.left, r.bottom - r.top); +} + +static void uiGroupDestroy(uiControl *c) +{ + uiGroup *g = uiGroup(c); + + if (g->child != NULL) { + uiControlSetParent(g->child, NULL); + uiControlDestroy(g->child); + } + uiWindowsEnsureDestroyWindow(g->hwnd); + uiFreeControl(uiControl(g)); +} + +uiWindowsControlDefaultHandle(uiGroup) +uiWindowsControlDefaultParent(uiGroup) +uiWindowsControlDefaultSetParent(uiGroup) +uiWindowsControlDefaultToplevel(uiGroup) +uiWindowsControlDefaultVisible(uiGroup) +uiWindowsControlDefaultShow(uiGroup) +uiWindowsControlDefaultHide(uiGroup) +uiWindowsControlDefaultEnabled(uiGroup) +uiWindowsControlDefaultEnable(uiGroup) +uiWindowsControlDefaultDisable(uiGroup) + +static void uiGroupSyncEnableState(uiWindowsControl *c, int enabled) +{ + uiGroup *g = uiGroup(c); + + if (uiWindowsShouldStopSyncEnableState(uiWindowsControl(g), enabled)) + return; + EnableWindow(g->hwnd, enabled); + if (g->child != NULL) + uiWindowsControlSyncEnableState(uiWindowsControl(g->child), enabled); +} + +uiWindowsControlDefaultSetParentHWND(uiGroup) + +static void uiGroupMinimumSize(uiWindowsControl *c, int *width, int *height) +{ + uiGroup *g = uiGroup(c); + int mx, mtop, mbottom; + int labelWidth; + + *width = 0; + *height = 0; + if (g->child != NULL) + uiWindowsControlMinimumSize(uiWindowsControl(g->child), width, height); + labelWidth = uiWindowsWindowTextWidth(g->hwnd); + if (*width < labelWidth) // don't clip the label; it doesn't ellipsize + *width = labelWidth; + groupMargins(g, &mx, &mtop, &mbottom); + *width += 2 * mx; + *height += mtop + mbottom; +} + +static void uiGroupMinimumSizeChanged(uiWindowsControl *c) +{ + uiGroup *g = uiGroup(c); + + if (uiWindowsControlTooSmall(uiWindowsControl(g))) { + uiWindowsControlContinueMinimumSizeChanged(uiWindowsControl(g)); + return; + } + groupRelayout(g); +} + +uiWindowsControlDefaultLayoutRect(uiGroup) +uiWindowsControlDefaultAssignControlIDZOrder(uiGroup) + +static void uiGroupChildVisibilityChanged(uiWindowsControl *c) +{ + // TODO eliminate the redundancy + uiWindowsControlMinimumSizeChanged(c); +} + +char *uiGroupTitle(uiGroup *g) +{ + return uiWindowsWindowText(g->hwnd); +} + +void uiGroupSetTitle(uiGroup *g, const char *text) +{ + uiWindowsSetWindowText(g->hwnd, text); + // changing the text might necessitate a change in the groupbox's size + uiWindowsControlMinimumSizeChanged(uiWindowsControl(g)); +} + +void uiGroupSetChild(uiGroup *g, uiControl *child) +{ + if (g->child != NULL) { + uiControlSetParent(g->child, NULL); + uiWindowsControlSetParentHWND(uiWindowsControl(g->child), NULL); + } + g->child = child; + if (g->child != NULL) { + uiControlSetParent(g->child, uiControl(g)); + uiWindowsControlSetParentHWND(uiWindowsControl(g->child), g->hwnd); + uiWindowsControlAssignSoleControlIDZOrder(uiWindowsControl(g->child)); + uiWindowsControlMinimumSizeChanged(uiWindowsControl(g)); + } +} + +int uiGroupMargined(uiGroup *g) +{ + return g->margined; +} + +void uiGroupSetMargined(uiGroup *g, int margined) +{ + g->margined = margined; + uiWindowsControlMinimumSizeChanged(uiWindowsControl(g)); +} + +static LRESULT CALLBACK groupSubProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) +{ + uiGroup *g = uiGroup(dwRefData); + WINDOWPOS *wp = (WINDOWPOS *) lParam; + MINMAXINFO *mmi = (MINMAXINFO *) lParam; + int minwid, minht; + LRESULT lResult; + + if (handleParentMessages(hwnd, uMsg, wParam, lParam, &lResult) != FALSE) + return lResult; + switch (uMsg) { + case WM_WINDOWPOSCHANGED: + if ((wp->flags & SWP_NOSIZE) != 0) + break; + groupRelayout(g); + return 0; + case WM_GETMINMAXINFO: + lResult = DefWindowProcW(hwnd, uMsg, wParam, lParam); + uiWindowsControlMinimumSize(uiWindowsControl(g), &minwid, &minht); + mmi->ptMinTrackSize.x = minwid; + mmi->ptMinTrackSize.y = minht; + return lResult; + case WM_NCDESTROY: + if (RemoveWindowSubclass(hwnd, groupSubProc, uIdSubclass) == FALSE) + logLastError(L"error removing groupbox subclass"); + break; + } + return DefSubclassProc(hwnd, uMsg, wParam, lParam); +} + +uiGroup *uiNewGroup(const char *text) +{ + uiGroup *g; + WCHAR *wtext; + + uiWindowsNewControl(uiGroup, g); + + wtext = toUTF16(text); + g->hwnd = uiWindowsEnsureCreateControlHWND(WS_EX_CONTROLPARENT, + L"button", wtext, + BS_GROUPBOX, + hInstance, NULL, + TRUE); + uiFree(wtext); + + if (SetWindowSubclass(g->hwnd, groupSubProc, 0, (DWORD_PTR) g) == FALSE) + logLastError(L"error subclassing groupbox to handle parent messages"); + + return g; +} diff --git a/src/libui_sdl/libui/windows/init.cpp b/src/libui_sdl/libui/windows/init.cpp new file mode 100644 index 0000000..2287416 --- /dev/null +++ b/src/libui_sdl/libui/windows/init.cpp @@ -0,0 +1,167 @@ +// 6 april 2015 +#include "uipriv_windows.hpp" + +HINSTANCE hInstance; +int nCmdShow; + +HFONT hMessageFont; + +// LONGTERM needed? +HBRUSH hollowBrush; + +// the returned pointer is actually to the second character +// if the first character is - then free, otherwise don't +static const char *initerr(const char *message, const WCHAR *label, DWORD value) +{ + WCHAR *sysmsg; + BOOL hassysmsg; + WCHAR *wmessage; + WCHAR *wout; + char *out; + + hassysmsg = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, value, 0, (LPWSTR) (&sysmsg), 0, NULL) != 0; + if (!hassysmsg) + sysmsg = L""; + wmessage = toUTF16(message + 1); + wout = strf(L"-error initializing libui: %s; code %I32d (0x%08I32X) %s", + wmessage, + value, value, + sysmsg); + uiFree(wmessage); + if (hassysmsg) + LocalFree(sysmsg); // ignore error + out = toUTF8(wout); + uiFree(wout); + return out + 1; +} + +#define ieLastErr(msg) initerr("=" msg, L"GetLastError() ==", GetLastError()) +#define ieHRESULT(msg, hr) initerr("=" msg, L"HRESULT", (DWORD) hr) + +// LONGTERM make common +uiInitOptions options; + +#define wantedICCClasses ( \ + ICC_STANDARD_CLASSES | /* user32.dll controls */ \ + ICC_PROGRESS_CLASS | /* progress bars */ \ + ICC_TAB_CLASSES | /* tabs */ \ + ICC_LISTVIEW_CLASSES | /* table headers */ \ + ICC_UPDOWN_CLASS | /* spinboxes */ \ + ICC_BAR_CLASSES | /* trackbar */ \ + ICC_DATE_CLASSES | /* date/time picker */ \ + 0) + +const char *uiInit(uiInitOptions *o) +{ + STARTUPINFOW si; + const char *ce; + HICON hDefaultIcon; + HCURSOR hDefaultCursor; + NONCLIENTMETRICSW ncm; + INITCOMMONCONTROLSEX icc; + HRESULT hr; + + options = *o; + + initAlloc(); + + nCmdShow = SW_SHOWDEFAULT; + GetStartupInfoW(&si); + if ((si.dwFlags & STARTF_USESHOWWINDOW) != 0) + nCmdShow = si.wShowWindow; + + // LONGTERM set DPI awareness + + hDefaultIcon = LoadIconW(NULL, IDI_APPLICATION); + if (hDefaultIcon == NULL) + return ieLastErr("loading default icon for window classes"); + hDefaultCursor = LoadCursorW(NULL, IDC_ARROW); + if (hDefaultCursor == NULL) + return ieLastErr("loading default cursor for window classes"); + + ce = initUtilWindow(hDefaultIcon, hDefaultCursor); + if (ce != NULL) + return initerr(ce, L"GetLastError() ==", GetLastError()); + + if (registerWindowClass(hDefaultIcon, hDefaultCursor) == 0) + return ieLastErr("registering uiWindow window class"); + + ZeroMemory(&ncm, sizeof (NONCLIENTMETRICSW)); + ncm.cbSize = sizeof (NONCLIENTMETRICSW); + if (SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, sizeof (NONCLIENTMETRICSW), &ncm, sizeof (NONCLIENTMETRICSW)) == 0) + return ieLastErr("getting default fonts"); + hMessageFont = CreateFontIndirectW(&(ncm.lfMessageFont)); + if (hMessageFont == NULL) + return ieLastErr("loading default messagebox font; this is the default UI font"); + + if (initContainer(hDefaultIcon, hDefaultCursor) == 0) + return ieLastErr("initializing uiWindowsMakeContainer() window class"); + + hollowBrush = (HBRUSH) GetStockObject(HOLLOW_BRUSH); + if (hollowBrush == NULL) + return ieLastErr("getting hollow brush"); + + ZeroMemory(&icc, sizeof (INITCOMMONCONTROLSEX)); + icc.dwSize = sizeof (INITCOMMONCONTROLSEX); + icc.dwICC = wantedICCClasses; + if (InitCommonControlsEx(&icc) == 0) + return ieLastErr("initializing Common Controls"); + + hr = CoInitialize(NULL); + if (hr != S_OK && hr != S_FALSE) + return ieHRESULT("initializing COM", hr); + // LONGTERM initialize COM security + // LONGTERM (windows vista) turn off COM exception handling + + hr = initDraw(); + if (hr != S_OK) + return ieHRESULT("initializing Direct2D", hr); + + hr = initDrawText(); + if (hr != S_OK) + return ieHRESULT("initializing DirectWrite", hr); + + if (registerAreaClass(hDefaultIcon, hDefaultCursor) == 0) + return ieLastErr("registering uiArea window class"); + + if (registerMessageFilter() == 0) + return ieLastErr("registering libui message filter"); + + if (registerD2DScratchClass(hDefaultIcon, hDefaultCursor) == 0) + return ieLastErr("initializing D2D scratch window class"); + + return NULL; +} + +void uiUninit(void) +{ + uninitMenus(); + unregisterD2DScratchClass(); + unregisterMessageFilter(); + unregisterArea(); + uninitDrawText(); + uninitDraw(); + CoUninitialize(); + if (DeleteObject(hollowBrush) == 0) + logLastError(L"error freeing hollow brush"); + uninitContainer(); + if (DeleteObject(hMessageFont) == 0) + logLastError(L"error deleting control font"); + unregisterWindowClass(); + // no need to delete the default icon or cursor; see http://stackoverflow.com/questions/30603077/ + uninitUtilWindow(); + uninitAlloc(); +} + +void uiFreeInitError(const char *err) +{ + if (*(err - 1) == '-') + uiFree((void *) (err - 1)); +} + +BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) +{ + if (fdwReason == DLL_PROCESS_ATTACH) + hInstance = hinstDLL; + return TRUE; +} diff --git a/src/libui_sdl/libui/windows/label.cpp b/src/libui_sdl/libui/windows/label.cpp new file mode 100644 index 0000000..d74b7d1 --- /dev/null +++ b/src/libui_sdl/libui/windows/label.cpp @@ -0,0 +1,57 @@ +// 11 april 2015 +#include "uipriv_windows.hpp" + +struct uiLabel { + uiWindowsControl c; + HWND hwnd; +}; + +uiWindowsControlAllDefaults(uiLabel) + +// via http://msdn.microsoft.com/en-us/library/windows/desktop/dn742486.aspx#sizingandspacing +#define labelHeight 8 + +static void uiLabelMinimumSize(uiWindowsControl *c, int *width, int *height) +{ + uiLabel *l = uiLabel(c); + uiWindowsSizing sizing; + int y; + + *width = uiWindowsWindowTextWidth(l->hwnd); + y = labelHeight; + uiWindowsGetSizing(l->hwnd, &sizing); + uiWindowsSizingDlgUnitsToPixels(&sizing, NULL, &y); + *height = y; +} + +char *uiLabelText(uiLabel *l) +{ + return uiWindowsWindowText(l->hwnd); +} + +void uiLabelSetText(uiLabel *l, const char *text) +{ + uiWindowsSetWindowText(l->hwnd, text); + // changing the text might necessitate a change in the label's size + uiWindowsControlMinimumSizeChanged(uiWindowsControl(l)); +} + +uiLabel *uiNewLabel(const char *text) +{ + uiLabel *l; + WCHAR *wtext; + + uiWindowsNewControl(uiLabel, l); + + wtext = toUTF16(text); + l->hwnd = uiWindowsEnsureCreateControlHWND(0, + L"static", wtext, + // SS_LEFTNOWORDWRAP clips text past the end; SS_NOPREFIX avoids accelerator translation + // controls are vertically aligned to the top by default (thanks Xeek in irc.freenode.net/#winapi) + SS_LEFTNOWORDWRAP | SS_NOPREFIX, + hInstance, NULL, + TRUE); + uiFree(wtext); + + return l; +} diff --git a/src/libui_sdl/libui/windows/libui.manifest b/src/libui_sdl/libui/windows/libui.manifest new file mode 100644 index 0000000..8beb6cf --- /dev/null +++ b/src/libui_sdl/libui/windows/libui.manifest @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> +<assemblyIdentity + version="1.0.0.0" + processorArchitecture="*" + name="CompanyName.ProductName.YourApplication" + type="win32" +/> +<description>Your application description here.</description> +<dependency> + <dependentAssembly> + <assemblyIdentity + type="win32" + name="Microsoft.Windows.Common-Controls" + version="6.0.0.0" + processorArchitecture="*" + publicKeyToken="6595b64144ccf1df" + language="*" + /> + </dependentAssembly> +</dependency> +<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> + <application> + <!--The ID below indicates application support for Windows Vista --> + <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/> + <!--The ID below indicates application support for Windows 7 --> + <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> + </application> +</compatibility> +</assembly> + diff --git a/src/libui_sdl/libui/windows/main.cpp b/src/libui_sdl/libui/windows/main.cpp new file mode 100644 index 0000000..eb6d849 --- /dev/null +++ b/src/libui_sdl/libui/windows/main.cpp @@ -0,0 +1,130 @@ +// 6 april 2015 +#include "uipriv_windows.hpp" + +static HHOOK filter; + +static LRESULT CALLBACK filterProc(int code, WPARAM wParam, LPARAM lParam) +{ + MSG *msg = (MSG *) lParam; + + if (code < 0) + goto callNext; + + if (areaFilter(msg)) // don't continue to our IsDialogMessage() hack if the area handled it + goto discard; + + // TODO IsDialogMessage() hack here + + // otherwise keep going + goto callNext; + +discard: + // we handled it; discard the message so the dialog manager doesn't see it + return 1; + +callNext: + return CallNextHookEx(filter, code, wParam, lParam); +} + +int registerMessageFilter(void) +{ + filter = SetWindowsHookExW(WH_MSGFILTER, + filterProc, + hInstance, + GetCurrentThreadId()); + return filter != NULL; +} + +void unregisterMessageFilter(void) +{ + if (UnhookWindowsHookEx(filter) == 0) + logLastError(L"error unregistering libui message filter"); +} + +// LONGTERM http://blogs.msdn.com/b/oldnewthing/archive/2005/04/08/406509.aspx when adding accelerators, TranslateAccelerators() before IsDialogMessage() + +static void processMessage(MSG *msg) +{ + HWND correctParent; + + if (msg->hwnd != NULL) + correctParent = parentToplevel(msg->hwnd); + else // just to be safe + correctParent = GetActiveWindow(); + if (correctParent != NULL) + // this calls our mesage filter above for us + if (IsDialogMessage(correctParent, msg) != 0) + return; + TranslateMessage(msg); + DispatchMessageW(msg); +} + +static int waitMessage(MSG *msg) +{ + int res; + + res = GetMessageW(msg, NULL, 0, 0); + if (res < 0) { + logLastError(L"error calling GetMessage()"); + return 0; // bail out on error + } + return res != 0; // returns false on WM_QUIT +} + +void uiMain(void) +{ + while (uiMainStep(1)) + ; +} + +void uiMainSteps(void) +{ + // don't need to do anything here +} + +static int peekMessage(MSG *msg) +{ + BOOL res; + + res = PeekMessageW(msg, NULL, 0, 0, PM_REMOVE); + if (res == 0) + return 2; // no message available + if (msg->message != WM_QUIT) + return 1; // a message + return 0; // WM_QUIT +} + +int uiMainStep(int wait) +{ + MSG msg; + + if (wait) { + if (!waitMessage(&msg)) + return 0; + processMessage(&msg); + return 1; + } + + // don't wait for a message + switch (peekMessage(&msg)) { + case 0: // quit + // TODO PostQuitMessage() again? + return 0; + case 1: // process a message + processMessage(&msg); + // fall out to the case for no message + } + return 1; // no message +} + +void uiQuit(void) +{ + PostQuitMessage(0); +} + +void uiQueueMain(void (*f)(void *data), void *data) +{ + if (PostMessageW(utilWindow, msgQueued, (WPARAM) f, (LPARAM) data) == 0) + // LONGTERM this is likely not safe to call across threads (allocates memory) + logLastError(L"error queueing function to run on main thread"); +} diff --git a/src/libui_sdl/libui/windows/menu.cpp b/src/libui_sdl/libui/windows/menu.cpp new file mode 100644 index 0000000..6112fc1 --- /dev/null +++ b/src/libui_sdl/libui/windows/menu.cpp @@ -0,0 +1,369 @@ +// 24 april 2015 +#include "uipriv_windows.hpp" + +// LONGTERM migrate to std::vector + +static uiMenu **menus = NULL; +static size_t len = 0; +static size_t cap = 0; +static BOOL menusFinalized = FALSE; +static WORD curID = 100; // start somewhere safe +static BOOL hasQuit = FALSE; +static BOOL hasPreferences = FALSE; +static BOOL hasAbout = FALSE; + +struct uiMenu { + WCHAR *name; + uiMenuItem **items; + size_t len; + size_t cap; +}; + +struct uiMenuItem { + WCHAR *name; + int type; + WORD id; + void (*onClicked)(uiMenuItem *, uiWindow *, void *); + void *onClickedData; + BOOL disabled; // template for new instances; kept in sync with everything else + BOOL checked; + HMENU *hmenus; + size_t len; + size_t cap; +}; + +enum { + typeRegular, + typeCheckbox, + typeQuit, + typePreferences, + typeAbout, + typeSeparator, +}; + +#define grow 32 + +static void sync(uiMenuItem *item) +{ + size_t i; + MENUITEMINFOW mi; + + ZeroMemory(&mi, sizeof (MENUITEMINFOW)); + mi.cbSize = sizeof (MENUITEMINFOW); + mi.fMask = MIIM_STATE; + if (item->disabled) + mi.fState |= MFS_DISABLED; + if (item->checked) + mi.fState |= MFS_CHECKED; + + for (i = 0; i < item->len; i++) + if (SetMenuItemInfo(item->hmenus[i], item->id, FALSE, &mi) == 0) + logLastError(L"error synchronizing menu items"); +} + +static void defaultOnClicked(uiMenuItem *item, uiWindow *w, void *data) +{ + // do nothing +} + +static void onQuitClicked(uiMenuItem *item, uiWindow *w, void *data) +{ + if (shouldQuit()) + uiQuit(); +} + +void uiMenuItemEnable(uiMenuItem *i) +{ + i->disabled = FALSE; + sync(i); +} + +void uiMenuItemDisable(uiMenuItem *i) +{ + i->disabled = TRUE; + sync(i); +} + +void uiMenuItemOnClicked(uiMenuItem *i, void (*f)(uiMenuItem *, uiWindow *, void *), void *data) +{ + if (i->type == typeQuit) + userbug("You can not call uiMenuItemOnClicked() on a Quit item; use uiOnShouldQuit() instead."); + i->onClicked = f; + i->onClickedData = data; +} + +int uiMenuItemChecked(uiMenuItem *i) +{ + return i->checked != FALSE; +} + +void uiMenuItemSetChecked(uiMenuItem *i, int checked) +{ + // use explicit values + i->checked = FALSE; + if (checked) + i->checked = TRUE; + sync(i); +} + +static uiMenuItem *newItem(uiMenu *m, int type, const char *name) +{ + uiMenuItem *item; + + if (menusFinalized) + userbug("You can not create a new menu item after menus have been finalized."); + + if (m->len >= m->cap) { + m->cap += grow; + m->items = (uiMenuItem **) uiRealloc(m->items, m->cap * sizeof (uiMenuItem *), "uiMenuitem *[]"); + } + + item = uiNew(uiMenuItem); + + m->items[m->len] = item; + m->len++; + + item->type = type; + switch (item->type) { + case typeQuit: + item->name = toUTF16("Quit"); + break; + case typePreferences: + item->name = toUTF16("Preferences..."); + break; + case typeAbout: + item->name = toUTF16("About"); + break; + case typeSeparator: + break; + default: + item->name = toUTF16(name); + break; + } + + if (item->type != typeSeparator) { + item->id = curID; + curID++; + } + + if (item->type == typeQuit) { + // can't call uiMenuItemOnClicked() here + item->onClicked = onQuitClicked; + item->onClickedData = NULL; + } else + uiMenuItemOnClicked(item, defaultOnClicked, NULL); + + return item; +} + +uiMenuItem *uiMenuAppendItem(uiMenu *m, const char *name) +{ + return newItem(m, typeRegular, name); +} + +uiMenuItem *uiMenuAppendCheckItem(uiMenu *m, const char *name) +{ + return newItem(m, typeCheckbox, name); +} + +uiMenuItem *uiMenuAppendQuitItem(uiMenu *m) +{ + if (hasQuit) + userbug("You can not have multiple Quit menu items in a program."); + hasQuit = TRUE; + newItem(m, typeSeparator, NULL); + return newItem(m, typeQuit, NULL); +} + +uiMenuItem *uiMenuAppendPreferencesItem(uiMenu *m) +{ + if (hasPreferences) + userbug("You can not have multiple Preferences menu items in a program."); + hasPreferences = TRUE; + newItem(m, typeSeparator, NULL); + return newItem(m, typePreferences, NULL); +} + +uiMenuItem *uiMenuAppendAboutItem(uiMenu *m) +{ + if (hasAbout) + // TODO place these userbug strings in a header + userbug("You can not have multiple About menu items in a program."); + hasAbout = TRUE; + newItem(m, typeSeparator, NULL); + return newItem(m, typeAbout, NULL); +} + +void uiMenuAppendSeparator(uiMenu *m) +{ + newItem(m, typeSeparator, NULL); +} + +uiMenu *uiNewMenu(const char *name) +{ + uiMenu *m; + + if (menusFinalized) + userbug("You can not create a new menu after menus have been finalized."); + if (len >= cap) { + cap += grow; + menus = (uiMenu **) uiRealloc(menus, cap * sizeof (uiMenu *), "uiMenu *[]"); + } + + m = uiNew(uiMenu); + + menus[len] = m; + len++; + + m->name = toUTF16(name); + + return m; +} + +static void appendMenuItem(HMENU menu, uiMenuItem *item) +{ + UINT uFlags; + + uFlags = MF_SEPARATOR; + if (item->type != typeSeparator) { + uFlags = MF_STRING; + if (item->disabled) + uFlags |= MF_DISABLED | MF_GRAYED; + if (item->checked) + uFlags |= MF_CHECKED; + } + if (AppendMenuW(menu, uFlags, item->id, item->name) == 0) + logLastError(L"error appending menu item"); + + if (item->len >= item->cap) { + item->cap += grow; + item->hmenus = (HMENU *) uiRealloc(item->hmenus, item->cap * sizeof (HMENU), "HMENU[]"); + } + item->hmenus[item->len] = menu; + item->len++; +} + +static HMENU makeMenu(uiMenu *m) +{ + HMENU menu; + size_t i; + + menu = CreatePopupMenu(); + if (menu == NULL) + logLastError(L"error creating menu"); + for (i = 0; i < m->len; i++) + appendMenuItem(menu, m->items[i]); + return menu; +} + +HMENU makeMenubar(void) +{ + HMENU menubar; + HMENU menu; + size_t i; + + menusFinalized = TRUE; + + menubar = CreateMenu(); + if (menubar == NULL) + logLastError(L"error creating menubar"); + + for (i = 0; i < len; i++) { + menu = makeMenu(menus[i]); + if (AppendMenuW(menubar, MF_POPUP | MF_STRING, (UINT_PTR) menu, menus[i]->name) == 0) + logLastError(L"error appending menu to menubar"); + } + + return menubar; +} + +void runMenuEvent(WORD id, uiWindow *w) +{ + uiMenu *m; + uiMenuItem *item; + size_t i, j; + + // this isn't optimal, but it works, and it should be just fine for most cases + for (i = 0; i < len; i++) { + m = menus[i]; + for (j = 0; j < m->len; j++) { + item = m->items[j]; + if (item->id == id) + goto found; + } + } + // no match + implbug("unknown menu ID %hu in runMenuEvent()", id); + +found: + // first toggle checkboxes, if any + if (item->type == typeCheckbox) + uiMenuItemSetChecked(item, !uiMenuItemChecked(item)); + + // then run the event + (*(item->onClicked))(item, w, item->onClickedData); +} + +static void freeMenu(uiMenu *m, HMENU submenu) +{ + size_t i; + uiMenuItem *item; + size_t j; + + for (i = 0; i < m->len; i++) { + item = m->items[i]; + for (j = 0; j < item->len; j++) + if (item->hmenus[j] == submenu) + break; + if (j >= item->len) + implbug("submenu handle %p not found in freeMenu()", submenu); + for (; j < item->len - 1; j++) + item->hmenus[j] = item->hmenus[j + 1]; + item->hmenus[j] = NULL; + item->len--; + } +} + +void freeMenubar(HMENU menubar) +{ + size_t i; + MENUITEMINFOW mi; + + for (i = 0; i < len; i++) { + ZeroMemory(&mi, sizeof (MENUITEMINFOW)); + mi.cbSize = sizeof (MENUITEMINFOW); + mi.fMask = MIIM_SUBMENU; + if (GetMenuItemInfoW(menubar, i, TRUE, &mi) == 0) + logLastError(L"error getting menu to delete item references from"); + freeMenu(menus[i], mi.hSubMenu); + } + // no need to worry about destroying any menus; destruction of the window they're in will do it for us +} + +void uninitMenus(void) +{ + uiMenu *m; + uiMenuItem *item; + size_t i, j; + + for (i = 0; i < len; i++) { + m = menus[i]; + uiFree(m->name); + for (j = 0; j < m->len; j++) { + item = m->items[j]; + if (item->len != 0) + // LONGTERM userbug()? + implbug("menu item %p (%ws) still has uiWindows attached; did you forget to destroy some windows?", item, item->name); + if (item->name != NULL) + uiFree(item->name); + if (item->hmenus != NULL) + uiFree(item->hmenus); + uiFree(item); + } + if (m->items != NULL) + uiFree(m->items); + uiFree(m); + } + if (menus != NULL) + uiFree(menus); +} diff --git a/src/libui_sdl/libui/windows/multilineentry.cpp b/src/libui_sdl/libui/windows/multilineentry.cpp new file mode 100644 index 0000000..a32960c --- /dev/null +++ b/src/libui_sdl/libui/windows/multilineentry.cpp @@ -0,0 +1,152 @@ +// 8 april 2015 +#include "uipriv_windows.hpp" + +// TODO there's alpha darkening of text going on in read-only ones; something is up in our parent logic + +struct uiMultilineEntry { + uiWindowsControl c; + HWND hwnd; + void (*onChanged)(uiMultilineEntry *, void *); + void *onChangedData; + BOOL inhibitChanged; +}; + +static BOOL onWM_COMMAND(uiControl *c, HWND hwnd, WORD code, LRESULT *lResult) +{ + uiMultilineEntry *e = uiMultilineEntry(c); + + if (code != EN_CHANGE) + return FALSE; + if (e->inhibitChanged) + return FALSE; + (*(e->onChanged))(e, e->onChangedData); + *lResult = 0; + return TRUE; +} + +static void uiMultilineEntryDestroy(uiControl *c) +{ + uiMultilineEntry *e = uiMultilineEntry(c); + + uiWindowsUnregisterWM_COMMANDHandler(e->hwnd); + uiWindowsEnsureDestroyWindow(e->hwnd); + uiFreeControl(uiControl(e)); +} + +uiWindowsControlAllDefaultsExceptDestroy(uiMultilineEntry) + +// from http://msdn.microsoft.com/en-us/library/windows/desktop/dn742486.aspx#sizingandspacing +#define entryWidth 107 /* this is actually the shorter progress bar width, but Microsoft only indicates as wide as necessary */ +// LONGTERM change this for multiline text boxes (longterm because how?) +#define entryHeight 14 + +static void uiMultilineEntryMinimumSize(uiWindowsControl *c, int *width, int *height) +{ + uiMultilineEntry *e = uiMultilineEntry(c); + uiWindowsSizing sizing; + int x, y; + + x = entryWidth; + y = entryHeight; + uiWindowsGetSizing(e->hwnd, &sizing); + uiWindowsSizingDlgUnitsToPixels(&sizing, &x, &y); + *width = x; + *height = y; +} + +static void defaultOnChanged(uiMultilineEntry *e, void *data) +{ + // do nothing +} + +char *uiMultilineEntryText(uiMultilineEntry *e) +{ + char *out; + + out = uiWindowsWindowText(e->hwnd); + CRLFtoLF(out); + return out; +} + +void uiMultilineEntrySetText(uiMultilineEntry *e, const char *text) +{ + char *crlf; + + // doing this raises an EN_CHANGED + e->inhibitChanged = TRUE; + crlf = LFtoCRLF(text); + uiWindowsSetWindowText(e->hwnd, text); + uiFree(crlf); + e->inhibitChanged = FALSE; + // don't queue the control for resize; entry sizes are independent of their contents +} + +void uiMultilineEntryAppend(uiMultilineEntry *e, const char *text) +{ + LRESULT n; + char *crlf; + WCHAR *wtext; + + // doing this raises an EN_CHANGED + e->inhibitChanged = TRUE; + // TODO preserve selection? caret? what if caret used to be at end? + // TODO scroll to bottom? + n = SendMessageW(e->hwnd, WM_GETTEXTLENGTH, 0, 0); + SendMessageW(e->hwnd, EM_SETSEL, n, n); + crlf = LFtoCRLF(text); + wtext = toUTF16(crlf); + uiFree(crlf); + SendMessageW(e->hwnd, EM_REPLACESEL, FALSE, (LPARAM) wtext); + uiFree(wtext); + e->inhibitChanged = FALSE; +} + +void uiMultilineEntryOnChanged(uiMultilineEntry *e, void (*f)(uiMultilineEntry *, void *), void *data) +{ + e->onChanged = f; + e->onChangedData = data; +} + +int uiMultilineEntryReadOnly(uiMultilineEntry *e) +{ + return (getStyle(e->hwnd) & ES_READONLY) != 0; +} + +void uiMultilineEntrySetReadOnly(uiMultilineEntry *e, int readonly) +{ + WPARAM ro; + + ro = (WPARAM) FALSE; + if (readonly) + ro = (WPARAM) TRUE; + if (SendMessage(e->hwnd, EM_SETREADONLY, ro, 0) == 0) + logLastError(L"error making uiMultilineEntry read-only"); +} + +static uiMultilineEntry *finishMultilineEntry(DWORD style) +{ + uiMultilineEntry *e; + + uiWindowsNewControl(uiMultilineEntry, e); + + e->hwnd = uiWindowsEnsureCreateControlHWND(WS_EX_CLIENTEDGE, + L"edit", L"", + ES_AUTOVSCROLL | ES_LEFT | ES_MULTILINE | ES_NOHIDESEL | ES_WANTRETURN | WS_TABSTOP | WS_VSCROLL | style, + hInstance, NULL, + TRUE); + + uiWindowsRegisterWM_COMMANDHandler(e->hwnd, onWM_COMMAND, uiControl(e)); + uiMultilineEntryOnChanged(e, defaultOnChanged, NULL); + + return e; +} + +uiMultilineEntry *uiNewMultilineEntry(void) +{ + return finishMultilineEntry(0); +} + +uiMultilineEntry *uiNewNonWrappingMultilineEntry(void) +{ + return finishMultilineEntry(WS_HSCROLL | ES_AUTOHSCROLL); +} diff --git a/src/libui_sdl/libui/windows/notes b/src/libui_sdl/libui/windows/notes new file mode 100644 index 0000000..f554dd2 --- /dev/null +++ b/src/libui_sdl/libui/windows/notes @@ -0,0 +1,3 @@ +DIALOGS +do not accelerate OK and Cancel buttons in dialogs + http://blogs.msdn.com/b/oldnewthing/archive/2008/05/08/8467905.aspx diff --git a/src/libui_sdl/libui/windows/parent.cpp b/src/libui_sdl/libui/windows/parent.cpp new file mode 100644 index 0000000..bde6fb9 --- /dev/null +++ b/src/libui_sdl/libui/windows/parent.cpp @@ -0,0 +1,144 @@ +// 26 april 2015 +#include "uipriv_windows.hpp" + +// This contains code used by all uiControls that contain other controls. +// It also contains the code to draw the background of a container.c container, as that is a variant of the WM_CTLCOLORxxx handler code. + +static HBRUSH parentBrush = NULL; + +static HWND parentWithBackground(HWND hwnd) +{ + HWND parent; + int cls; + + parent = hwnd; + for (;;) { + parent = parentOf(parent); + // skip groupboxes; they're (supposed to be) transparent + // skip uiContainers; they don't draw anything + cls = windowClassOf(parent, L"button", containerClass, NULL); + if (cls != 0 && cls != 1) + break; + } + return parent; +} + +struct parentDraw { + HDC cdc; + HBITMAP bitmap; + HBITMAP prevbitmap; +}; + +static HRESULT parentDraw(HDC dc, HWND parent, struct parentDraw *pd) +{ + RECT r; + + uiWindowsEnsureGetClientRect(parent, &r); + pd->cdc = CreateCompatibleDC(dc); + if (pd->cdc == NULL) + return logLastError(L"error creating compatible DC"); + pd->bitmap = CreateCompatibleBitmap(dc, r.right - r.left, r.bottom - r.top); + if (pd->bitmap == NULL) + return logLastError(L"error creating compatible bitmap"); + pd->prevbitmap = (HBITMAP) SelectObject(pd->cdc, pd->bitmap); + if (pd->prevbitmap == NULL) + return logLastError(L"error selecting bitmap into compatible DC"); + SendMessageW(parent, WM_PRINTCLIENT, (WPARAM) (pd->cdc), PRF_CLIENT); + return S_OK; +} + +static void endParentDraw(struct parentDraw *pd) +{ + // continue in case of any error + if (pd->prevbitmap != NULL) + if (((HBITMAP) SelectObject(pd->cdc, pd->prevbitmap)) != pd->bitmap) + logLastError(L"error selecting previous bitmap back into compatible DC"); + if (pd->bitmap != NULL) + if (DeleteObject(pd->bitmap) == 0) + logLastError(L"error deleting compatible bitmap"); + if (pd->cdc != NULL) + if (DeleteDC(pd->cdc) == 0) + logLastError(L"error deleting compatible DC"); +} + +// see http://www.codeproject.com/Articles/5978/Correctly-drawn-themed-dialogs-in-WinXP +static HBRUSH getControlBackgroundBrush(HWND hwnd, HDC dc) +{ + HWND parent; + RECT hwndScreenRect; + struct parentDraw pd; + HBRUSH brush; + HRESULT hr; + + parent = parentWithBackground(hwnd); + + hr = parentDraw(dc, parent, &pd); + if (hr != S_OK) + return NULL; + brush = CreatePatternBrush(pd.bitmap); + if (brush == NULL) { + logLastError(L"error creating pattern brush"); + endParentDraw(&pd); + return NULL; + } + endParentDraw(&pd); + + // now figure out where the control is relative to the parent so we can align the brush properly + // if anything fails, give up and return the brush as-is + uiWindowsEnsureGetWindowRect(hwnd, &hwndScreenRect); + // this will be in screen coordinates; convert to parent coordinates + mapWindowRect(NULL, parent, &hwndScreenRect); + if (SetBrushOrgEx(dc, -hwndScreenRect.left, -hwndScreenRect.top, NULL) == 0) + logLastError(L"error setting brush origin"); + + return brush; +} + +void paintContainerBackground(HWND hwnd, HDC dc, RECT *paintRect) +{ + HWND parent; + RECT paintRectParent; + struct parentDraw pd; + HRESULT hr; + + parent = parentWithBackground(hwnd); + hr = parentDraw(dc, parent, &pd); + if (hr != S_OK) // we couldn't get it; draw nothing + return; + + paintRectParent = *paintRect; + mapWindowRect(hwnd, parent, &paintRectParent); + if (BitBlt(dc, paintRect->left, paintRect->top, paintRect->right - paintRect->left, paintRect->bottom - paintRect->top, + pd.cdc, paintRectParent.left, paintRectParent.top, + SRCCOPY) == 0) + logLastError(L"error drawing parent background over uiContainer"); + + endParentDraw(&pd); +} + +// TODO make this public if we want custom containers +// why have this to begin with? http://blogs.msdn.com/b/oldnewthing/archive/2010/03/16/9979112.aspx +BOOL handleParentMessages(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT *lResult) +{ + switch (uMsg) { + case WM_COMMAND: + return runWM_COMMAND(wParam, lParam, lResult); + case WM_NOTIFY: + return runWM_NOTIFY(wParam, lParam, lResult); + case WM_HSCROLL: + return runWM_HSCROLL(wParam, lParam, lResult); + case WM_CTLCOLORSTATIC: + case WM_CTLCOLORBTN: + if (parentBrush != NULL) + if (DeleteObject(parentBrush) == 0) + logLastError(L"error deleting old background brush()"); // but continue anyway; we will leak a brush but whatever + if (SetBkMode((HDC) wParam, TRANSPARENT) == 0) + logLastError(L"error setting transparent background mode to controls"); // but continue anyway; text will be wrong + parentBrush = getControlBackgroundBrush((HWND) lParam, (HDC) wParam); + if (parentBrush == NULL) // failed; just do default behavior + return FALSE; + *lResult = (LRESULT) parentBrush; + return TRUE; + } + return FALSE; +} diff --git a/src/libui_sdl/libui/windows/progressbar.cpp b/src/libui_sdl/libui/windows/progressbar.cpp new file mode 100644 index 0000000..3750eb6 --- /dev/null +++ b/src/libui_sdl/libui/windows/progressbar.cpp @@ -0,0 +1,83 @@ +// 19 may 2015 +#include "uipriv_windows.hpp" + +struct uiProgressBar { + uiWindowsControl c; + HWND hwnd; +}; + +uiWindowsControlAllDefaults(uiProgressBar) + +// via http://msdn.microsoft.com/en-us/library/windows/desktop/dn742486.aspx#sizingandspacing +#define pbarWidth 237 +#define pbarHeight 8 + +static void uiProgressBarMinimumSize(uiWindowsControl *c, int *width, int *height) +{ + uiProgressBar *p = uiProgressBar(c); + uiWindowsSizing sizing; + int x, y; + + x = pbarWidth; + y = pbarHeight; + uiWindowsGetSizing(p->hwnd, &sizing); + uiWindowsSizingDlgUnitsToPixels(&sizing, &x, &y); + *width = x; + *height = y; +} + +#define indeterminate(p) ((getStyle(p->hwnd) & PBS_MARQUEE) != 0) + +int uiProgressBarValue(uiProgressBar *p) +{ + if (indeterminate(p)) + return -1; + return SendMessage(p->hwnd, PBM_GETPOS, 0, 0); +} + +// unfortunately, as of Vista progress bars have a forced animation on increase +// we have to set the progress bar to value + 1 and decrease it back to value if we want an "instant" change +// see http://stackoverflow.com/questions/2217688/windows-7-aero-theme-progress-bar-bug +// it's not ideal/perfect, but it will have to do +void uiProgressBarSetValue(uiProgressBar *p, int value) +{ + if (value == -1) { + if (!indeterminate(p)) { + setStyle(p->hwnd, getStyle(p->hwnd) | PBS_MARQUEE); + SendMessageW(p->hwnd, PBM_SETMARQUEE, (WPARAM) TRUE, 0); + } + return; + } + if (indeterminate(p)) { + SendMessageW(p->hwnd, PBM_SETMARQUEE, (WPARAM) FALSE, 0); + setStyle(p->hwnd, getStyle(p->hwnd) & ~PBS_MARQUEE); + } + + if (value < 0 || value > 100) + userbug("Value %d is out of range for uiProgressBars.", value); + + if (value == 100) { // because we can't 101 + SendMessageW(p->hwnd, PBM_SETRANGE32, 0, 101); + SendMessageW(p->hwnd, PBM_SETPOS, 101, 0); + SendMessageW(p->hwnd, PBM_SETPOS, 100, 0); + SendMessageW(p->hwnd, PBM_SETRANGE32, 0, 100); + return; + } + SendMessageW(p->hwnd, PBM_SETPOS, (WPARAM) (value + 1), 0); + SendMessageW(p->hwnd, PBM_SETPOS, (WPARAM) value, 0); +} + +uiProgressBar *uiNewProgressBar(void) +{ + uiProgressBar *p; + + uiWindowsNewControl(uiProgressBar, p); + + p->hwnd = uiWindowsEnsureCreateControlHWND(0, + PROGRESS_CLASSW, L"", + PBS_SMOOTH, + hInstance, NULL, + FALSE); + + return p; +} diff --git a/src/libui_sdl/libui/windows/radiobuttons.cpp b/src/libui_sdl/libui/windows/radiobuttons.cpp new file mode 100644 index 0000000..29cd2e6 --- /dev/null +++ b/src/libui_sdl/libui/windows/radiobuttons.cpp @@ -0,0 +1,196 @@ +// 20 may 2015 +#include "uipriv_windows.hpp" + +// desired behavior: +// - tab moves between the radio buttons and the adjacent controls +// - arrow keys navigate between radio buttons +// - arrow keys do not leave the radio buttons (this is done in control.c) +// - arrow keys wrap around bare groups (if the previous control has WS_GROUP but the first radio button doesn't, then it doesn't; since our radio buttons are all in their own child window we can't do that) +// - clicking on a radio button draws a focus rect (TODO) + +struct uiRadioButtons { + uiWindowsControl c; + HWND hwnd; // of the container + std::vector<HWND> *hwnds; // of the buttons + void (*onSelected)(uiRadioButtons *, void *); + void *onSelectedData; +}; + +static BOOL onWM_COMMAND(uiControl *c, HWND clicked, WORD code, LRESULT *lResult) +{ + uiRadioButtons *r = uiRadioButtons(c); + WPARAM check; + + if (code != BN_CLICKED) + return FALSE; + for (const HWND &hwnd : *(r->hwnds)) { + check = BST_UNCHECKED; + if (clicked == hwnd) + check = BST_CHECKED; + SendMessage(hwnd, BM_SETCHECK, check, 0); + } + (*(r->onSelected))(r, r->onSelectedData); + *lResult = 0; + return TRUE; +} + +static void defaultOnSelected(uiRadioButtons *r, void *data) +{ + // do nothing +} + +static void uiRadioButtonsDestroy(uiControl *c) +{ + uiRadioButtons *r = uiRadioButtons(c); + + for (const HWND &hwnd : *(r->hwnds)) { + uiWindowsUnregisterWM_COMMANDHandler(hwnd); + uiWindowsEnsureDestroyWindow(hwnd); + } + delete r->hwnds; + uiWindowsEnsureDestroyWindow(r->hwnd); + uiFreeControl(uiControl(r)); +} + +// TODO SyncEnableState +uiWindowsControlAllDefaultsExceptDestroy(uiRadioButtons) + +// from http://msdn.microsoft.com/en-us/library/windows/desktop/dn742486.aspx#sizingandspacing +#define radiobuttonHeight 10 +// from http://msdn.microsoft.com/en-us/library/windows/desktop/bb226818%28v=vs.85%29.aspx +#define radiobuttonXFromLeftOfBoxToLeftOfLabel 12 + +static void uiRadioButtonsMinimumSize(uiWindowsControl *c, int *width, int *height) +{ + uiRadioButtons *r = uiRadioButtons(c); + int wid, maxwid; + uiWindowsSizing sizing; + int x, y; + + if (r->hwnds->size() == 0) { + *width = 0; + *height = 0; + return; + } + maxwid = 0; + for (const HWND &hwnd : *(r->hwnds)) { + wid = uiWindowsWindowTextWidth(hwnd); + if (maxwid < wid) + maxwid = wid; + } + + x = radiobuttonXFromLeftOfBoxToLeftOfLabel; + y = radiobuttonHeight; + uiWindowsGetSizing((*(r->hwnds))[0], &sizing); + uiWindowsSizingDlgUnitsToPixels(&sizing, &x, &y); + + *width = x + maxwid; + *height = y * r->hwnds->size(); +} + +static void radiobuttonsRelayout(uiRadioButtons *r) +{ + RECT client; + int x, y, width, height; + int height1; + uiWindowsSizing sizing; + + if (r->hwnds->size() == 0) + return; + uiWindowsEnsureGetClientRect(r->hwnd, &client); + x = client.left; + y = client.top; + width = client.right - client.left; + height1 = radiobuttonHeight; + uiWindowsGetSizing((*(r->hwnds))[0], &sizing); + uiWindowsSizingDlgUnitsToPixels(&sizing, NULL, &height1); + height = height1; + for (const HWND &hwnd : *(r->hwnds)) { + uiWindowsEnsureMoveWindowDuringResize(hwnd, x, y, width, height); + y += height; + } +} + +static void radiobuttonsArrangeChildren(uiRadioButtons *r) +{ + LONG_PTR controlID; + HWND insertAfter; + + controlID = 100; + insertAfter = NULL; + for (const HWND &hwnd : *(r->hwnds)) + uiWindowsEnsureAssignControlIDZOrder(hwnd, &controlID, &insertAfter); +} + +void uiRadioButtonsAppend(uiRadioButtons *r, const char *text) +{ + HWND hwnd; + WCHAR *wtext; + DWORD groupTabStop; + + // the first radio button gets both WS_GROUP and WS_TABSTOP + // successive radio buttons get *neither* + groupTabStop = 0; + if (r->hwnds->size() == 0) + groupTabStop = WS_GROUP | WS_TABSTOP; + + wtext = toUTF16(text); + hwnd = uiWindowsEnsureCreateControlHWND(0, + L"button", wtext, + BS_RADIOBUTTON | groupTabStop, + hInstance, NULL, + TRUE); + uiFree(wtext); + uiWindowsEnsureSetParentHWND(hwnd, r->hwnd); + uiWindowsRegisterWM_COMMANDHandler(hwnd, onWM_COMMAND, uiControl(r)); + r->hwnds->push_back(hwnd); + radiobuttonsArrangeChildren(r); + uiWindowsControlMinimumSizeChanged(uiWindowsControl(r)); +} + +int uiRadioButtonsSelected(uiRadioButtons *r) +{ + size_t i; + + for (i = 0; i < r->hwnds->size(); i++) + if (SendMessage((*(r->hwnds))[i], BM_GETCHECK, 0, 0) == BST_CHECKED) + return i; + return -1; +} + +void uiRadioButtonsSetSelected(uiRadioButtons *r, int n) +{ + int m; + + m = uiRadioButtonsSelected(r); + if (m != -1) + SendMessage((*(r->hwnds))[m], BM_SETCHECK, BST_UNCHECKED, 0); + if (n != -1) + SendMessage((*(r->hwnds))[n], BM_SETCHECK, BST_CHECKED, 0); +} + +void uiRadioButtonsOnSelected(uiRadioButtons *r, void (*f)(uiRadioButtons *, void *), void *data) +{ + r->onSelected = f; + r->onSelectedData = data; +} + +static void onResize(uiWindowsControl *c) +{ + radiobuttonsRelayout(uiRadioButtons(c)); +} + +uiRadioButtons *uiNewRadioButtons(void) +{ + uiRadioButtons *r; + + uiWindowsNewControl(uiRadioButtons, r); + + r->hwnd = uiWindowsMakeContainer(uiWindowsControl(r), onResize); + + r->hwnds = new std::vector<HWND>; + + uiRadioButtonsOnSelected(r, defaultOnSelected, NULL); + + return r; +} diff --git a/src/libui_sdl/libui/windows/resources.hpp b/src/libui_sdl/libui/windows/resources.hpp new file mode 100644 index 0000000..4ae5472 --- /dev/null +++ b/src/libui_sdl/libui/windows/resources.hpp @@ -0,0 +1,37 @@ +// 30 may 2015 + +#define rcTabPageDialog 29000 +#define rcFontDialog 29001 +#define rcColorDialog 29002 + +// TODO normalize these + +#define rcFontFamilyCombobox 1000 +#define rcFontStyleCombobox 1001 +#define rcFontSizeCombobox 1002 +#define rcFontSamplePlacement 1003 + +#define rcColorSVChooser 1100 +#define rcColorHSlider 1101 +#define rcPreview 1102 +#define rcOpacitySlider 1103 +#define rcH 1104 +#define rcS 1105 +#define rcV 1106 +#define rcRDouble 1107 +#define rcRInt 1108 +#define rcGDouble 1109 +#define rcGInt 1110 +#define rcBDouble 1111 +#define rcBInt 1112 +#define rcADouble 1113 +#define rcAInt 1114 +#define rcHex 1115 +#define rcHLabel 1116 +#define rcSLabel 1117 +#define rcVLabel 1118 +#define rcRLabel 1119 +#define rcGLabel 1120 +#define rcBLabel 1121 +#define rcALabel 1122 +#define rcHexLabel 1123 diff --git a/src/libui_sdl/libui/windows/resources.rc b/src/libui_sdl/libui/windows/resources.rc new file mode 100644 index 0000000..989dfc9 --- /dev/null +++ b/src/libui_sdl/libui/windows/resources.rc @@ -0,0 +1,96 @@ +// 30 may 2015 +#include "winapi.hpp" +#include "resources.hpp" + +// this is a UTF-8 file +#pragma code_page(65001) + +// this is the Common Controls 6 manifest +// we only define it in a shared build; static builds have to include the appropriate parts of the manifest in the output executable +// LONGTERM set up the string values here +#ifndef _UI_STATIC +ISOLATIONAWARE_MANIFEST_RESOURCE_ID RT_MANIFEST "libui.manifest" +#endif + +// this is the dialog template used by tab pages; see windows/tabpage.c for details +rcTabPageDialog DIALOGEX 0, 0, 100, 100 +STYLE DS_CONTROL | WS_CHILD | WS_VISIBLE +EXSTYLE WS_EX_CONTROLPARENT +BEGIN + // nothing +END + +// this is for our custom DirectWrite-based font dialog (see fontdialog.cpp) +// this is based on the "New Font Dialog with Syslink" in Microsoft's font.dlg +// LONGTERM look at localization +// LONGTERM make it look tighter and nicer like the real one, including the actual heights of the font family and style comboboxes +rcFontDialog DIALOGEX 13, 54, 243, 200 +STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU | DS_3DLOOK +CAPTION "Font" +FONT 9, "Segoe UI" +BEGIN + LTEXT "&Font:", -1, 7, 7, 98, 9 + COMBOBOX rcFontFamilyCombobox, 7, 16, 98, 76, + CBS_SIMPLE | CBS_AUTOHSCROLL | CBS_DISABLENOSCROLL | + CBS_SORT | WS_VSCROLL | WS_TABSTOP | CBS_HASSTRINGS + + LTEXT "Font st&yle:", -1, 114, 7, 74, 9 + COMBOBOX rcFontStyleCombobox, 114, 16, 74, 76, + CBS_SIMPLE | CBS_AUTOHSCROLL | CBS_DISABLENOSCROLL | + WS_VSCROLL | WS_TABSTOP | CBS_HASSTRINGS + + LTEXT "&Size:", -1, 198, 7, 36, 9 + COMBOBOX rcFontSizeCombobox, 198, 16, 36, 76, + CBS_SIMPLE | CBS_AUTOHSCROLL | CBS_DISABLENOSCROLL | + CBS_SORT | WS_VSCROLL | WS_TABSTOP | CBS_HASSTRINGS + + GROUPBOX "Sample", -1, 7, 97, 227, 70, WS_GROUP + CTEXT "AaBbYyZz", rcFontSamplePlacement, 9, 106, 224, 60, SS_NOPREFIX | NOT WS_VISIBLE + + DEFPUSHBUTTON "OK", IDOK, 141, 181, 45, 14, WS_GROUP + PUSHBUTTON "Cancel", IDCANCEL, 190, 181, 45, 14, WS_GROUP +END + +rcColorDialog DIALOGEX 13, 54, 344, 209 +STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU | DS_3DLOOK +CAPTION "Color" +FONT 9, "Segoe UI" +BEGIN + // this size should be big enough to get at least 256x256 on font sizes >= 8 pt + CTEXT "AaBbYyZz", rcColorSVChooser, 7, 7, 195, 195, SS_NOPREFIX | SS_BLACKRECT + + // width is the suggested slider height since this is vertical + CTEXT "AaBbYyZz", rcColorHSlider, 206, 7, 15, 195, SS_NOPREFIX | SS_BLACKRECT + + LTEXT "Preview:", -1, 230, 7, 107, 9, SS_NOPREFIX + CTEXT "AaBbYyZz", rcPreview, 230, 16, 107, 20, SS_NOPREFIX | SS_BLACKRECT + + LTEXT "Opacity:", -1, 230, 45, 107, 9, SS_NOPREFIX + CTEXT "AaBbYyZz", rcOpacitySlider, 230, 54, 107, 15, SS_NOPREFIX | SS_BLACKRECT + + LTEXT "&H:", rcHLabel, 230, 81, 8, 8 + EDITTEXT rcH, 238, 78, 30, 14, ES_LEFT | ES_AUTOHSCROLL | WS_TABSTOP, WS_EX_CLIENTEDGE + LTEXT "&S:", rcSLabel, 230, 95, 8, 8 + EDITTEXT rcS, 238, 92, 30, 14, ES_LEFT | ES_AUTOHSCROLL | WS_TABSTOP, WS_EX_CLIENTEDGE + LTEXT "&V:", rcVLabel, 230, 109, 8, 8 + EDITTEXT rcV, 238, 106, 30, 14, ES_LEFT | ES_AUTOHSCROLL | WS_TABSTOP, WS_EX_CLIENTEDGE + + LTEXT "&R:", rcRLabel, 277, 81, 8, 8 + EDITTEXT rcRDouble, 285, 78, 30, 14, ES_LEFT | ES_AUTOHSCROLL | WS_TABSTOP, WS_EX_CLIENTEDGE + EDITTEXT rcRInt, 315, 78, 20, 14, ES_LEFT | ES_AUTOHSCROLL | ES_NUMBER | WS_TABSTOP, WS_EX_CLIENTEDGE + LTEXT "&G:", rcGLabel, 277, 95, 8, 8 + EDITTEXT rcGDouble, 285, 92, 30, 14, ES_LEFT | ES_AUTOHSCROLL | WS_TABSTOP, WS_EX_CLIENTEDGE + EDITTEXT rcGInt, 315, 92, 20, 14, ES_LEFT | ES_AUTOHSCROLL | ES_NUMBER | WS_TABSTOP, WS_EX_CLIENTEDGE + LTEXT "&B:", rcBLabel, 277, 109, 8, 8 + EDITTEXT rcBDouble, 285, 106, 30, 14, ES_LEFT | ES_AUTOHSCROLL | WS_TABSTOP, WS_EX_CLIENTEDGE + EDITTEXT rcBInt, 315, 106, 20, 14, ES_LEFT | ES_AUTOHSCROLL | ES_NUMBER | WS_TABSTOP, WS_EX_CLIENTEDGE + LTEXT "&A:", rcALabel, 277, 123, 8, 8 + EDITTEXT rcADouble, 285, 120, 30, 14, ES_LEFT | ES_AUTOHSCROLL | WS_TABSTOP, WS_EX_CLIENTEDGE + EDITTEXT rcAInt, 315, 120, 20, 14, ES_LEFT | ES_AUTOHSCROLL | ES_NUMBER | WS_TABSTOP, WS_EX_CLIENTEDGE + + LTEXT "He&x:", rcHexLabel, 269, 146, 16, 8 + EDITTEXT rcHex, 285, 143, 50, 14, ES_LEFT | ES_AUTOHSCROLL | WS_TABSTOP, WS_EX_CLIENTEDGE + + DEFPUSHBUTTON "OK", IDOK, 243, 188, 45, 14, WS_GROUP + PUSHBUTTON "Cancel", IDCANCEL, 292, 188, 45, 14, WS_GROUP +END diff --git a/src/libui_sdl/libui/windows/separator.cpp b/src/libui_sdl/libui/windows/separator.cpp new file mode 100644 index 0000000..e123e27 --- /dev/null +++ b/src/libui_sdl/libui/windows/separator.cpp @@ -0,0 +1,69 @@ +// 20 may 2015 +#include "uipriv_windows.hpp" + +// references: +// - http://stackoverflow.com/questions/2892703/how-do-i-draw-separators +// - https://msdn.microsoft.com/en-us/library/windows/desktop/dn742405%28v=vs.85%29.aspx + +struct uiSeparator { + uiWindowsControl c; + HWND hwnd; + BOOL vertical; +}; + +uiWindowsControlAllDefaults(uiSeparator) + +// via https://msdn.microsoft.com/en-us/library/windows/desktop/bb226818%28v=vs.85%29.aspx +#define separatorHeight 1 + +// TODO +#define separatorWidth 1 + +static void uiSeparatorMinimumSize(uiWindowsControl *c, int *width, int *height) +{ + uiSeparator *s = uiSeparator(c); + uiWindowsSizing sizing; + int x, y; + + *width = 1; // TODO + *height = 1; + x = separatorWidth; + y = separatorHeight; + uiWindowsGetSizing(s->hwnd, &sizing); + uiWindowsSizingDlgUnitsToPixels(&sizing, &x, &y); + if (s->vertical) + *width = x; + else + *height = y; +} + +uiSeparator *uiNewHorizontalSeparator(void) +{ + uiSeparator *s; + + uiWindowsNewControl(uiSeparator, s); + + s->hwnd = uiWindowsEnsureCreateControlHWND(0, + L"static", L"", + SS_ETCHEDHORZ, + hInstance, NULL, + TRUE); + + return s; +} + +uiSeparator *uiNewVerticalSeparator(void) +{ + uiSeparator *s; + + uiWindowsNewControl(uiSeparator, s); + + s->hwnd = uiWindowsEnsureCreateControlHWND(0, + L"static", L"", + SS_ETCHEDHORZ, + hInstance, NULL, + TRUE); + s->vertical = TRUE; + + return s; +} diff --git a/src/libui_sdl/libui/windows/sizing.cpp b/src/libui_sdl/libui/windows/sizing.cpp new file mode 100644 index 0000000..a6d25d6 --- /dev/null +++ b/src/libui_sdl/libui/windows/sizing.cpp @@ -0,0 +1,62 @@ +// 14 may 2015 +#include "uipriv_windows.hpp" + +// TODO rework the error handling +void getSizing(HWND hwnd, uiWindowsSizing *sizing, HFONT font) +{ + HDC dc; + HFONT prevfont; + TEXTMETRICW tm; + SIZE size; + + dc = GetDC(hwnd); + if (dc == NULL) + logLastError(L"error getting DC"); + prevfont = (HFONT) SelectObject(dc, font); + if (prevfont == NULL) + logLastError(L"error loading control font into device context"); + + ZeroMemory(&tm, sizeof (TEXTMETRICW)); + if (GetTextMetricsW(dc, &tm) == 0) + logLastError(L"error getting text metrics"); + if (GetTextExtentPoint32W(dc, L"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 52, &size) == 0) + logLastError(L"error getting text extent point"); + + sizing->BaseX = (int) ((size.cx / 26 + 1) / 2); + sizing->BaseY = (int) tm.tmHeight; + sizing->InternalLeading = tm.tmInternalLeading; + + if (SelectObject(dc, prevfont) != font) + logLastError(L"error restoring previous font into device context"); + if (ReleaseDC(hwnd, dc) == 0) + logLastError(L"error releasing DC"); +} + +void uiWindowsGetSizing(HWND hwnd, uiWindowsSizing *sizing) +{ + return getSizing(hwnd, sizing, hMessageFont); +} + +#define dlgUnitsToX(dlg, baseX) MulDiv((dlg), (baseX), 4) +#define dlgUnitsToY(dlg, baseY) MulDiv((dlg), (baseY), 8) + +void uiWindowsSizingDlgUnitsToPixels(uiWindowsSizing *sizing, int *x, int *y) +{ + if (x != NULL) + *x = dlgUnitsToX(*x, sizing->BaseX); + if (y != NULL) + *y = dlgUnitsToY(*y, sizing->BaseY); +} + +// from https://msdn.microsoft.com/en-us/library/windows/desktop/dn742486.aspx#sizingandspacing and https://msdn.microsoft.com/en-us/library/windows/desktop/bb226818%28v=vs.85%29.aspx +// this X value is really only for buttons but I don't see a better one :/ +#define winXPadding 4 +#define winYPadding 4 + +void uiWindowsSizingStandardPadding(uiWindowsSizing *sizing, int *x, int *y) +{ + if (x != NULL) + *x = dlgUnitsToX(winXPadding, sizing->BaseX); + if (y != NULL) + *y = dlgUnitsToY(winYPadding, sizing->BaseY); +} diff --git a/src/libui_sdl/libui/windows/slider.cpp b/src/libui_sdl/libui/windows/slider.cpp new file mode 100644 index 0000000..5c671dd --- /dev/null +++ b/src/libui_sdl/libui/windows/slider.cpp @@ -0,0 +1,98 @@ +// 20 may 2015 +#include "uipriv_windows.hpp" + +struct uiSlider { + uiWindowsControl c; + HWND hwnd; + void (*onChanged)(uiSlider *, void *); + void *onChangedData; +}; + +static BOOL onWM_HSCROLL(uiControl *c, HWND hwnd, WORD code, LRESULT *lResult) +{ + uiSlider *s = uiSlider(c); + + (*(s->onChanged))(s, s->onChangedData); + *lResult = 0; + return TRUE; +} + +static void uiSliderDestroy(uiControl *c) +{ + uiSlider *s = uiSlider(c); + + uiWindowsUnregisterWM_HSCROLLHandler(s->hwnd); + uiWindowsEnsureDestroyWindow(s->hwnd); + uiFreeControl(uiControl(s)); +} + +uiWindowsControlAllDefaultsExceptDestroy(uiSlider); + +// from http://msdn.microsoft.com/en-us/library/windows/desktop/dn742486.aspx#sizingandspacing +#define sliderWidth 107 /* this is actually the shorter progress bar width, but Microsoft doesn't indicate a width */ +#define sliderHeight 15 + +static void uiSliderMinimumSize(uiWindowsControl *c, int *width, int *height) +{ + uiSlider *s = uiSlider(c); + uiWindowsSizing sizing; + int x, y; + + x = sliderWidth; + y = sliderHeight; + uiWindowsGetSizing(s->hwnd, &sizing); + uiWindowsSizingDlgUnitsToPixels(&sizing, &x, &y); + *width = x; + *height = y; +} + +static void defaultOnChanged(uiSlider *s, void *data) +{ + // do nothing +} + +int uiSliderValue(uiSlider *s) +{ + return SendMessageW(s->hwnd, TBM_GETPOS, 0, 0); +} + +void uiSliderSetValue(uiSlider *s, int value) +{ + // don't use TBM_SETPOSNOTIFY; that triggers an event + SendMessageW(s->hwnd, TBM_SETPOS, (WPARAM) TRUE, (LPARAM) value); +} + +void uiSliderOnChanged(uiSlider *s, void (*f)(uiSlider *, void *), void *data) +{ + s->onChanged = f; + s->onChangedData = data; +} + +uiSlider *uiNewSlider(int min, int max) +{ + uiSlider *s; + int temp; + + if (min >= max) { + temp = min; + min = max; + max = temp; + } + + uiWindowsNewControl(uiSlider, s); + + s->hwnd = uiWindowsEnsureCreateControlHWND(0, + TRACKBAR_CLASSW, L"", + TBS_HORZ | TBS_TOOLTIPS | TBS_TRANSPARENTBKGND | WS_TABSTOP, + hInstance, NULL, + TRUE); + + uiWindowsRegisterWM_HSCROLLHandler(s->hwnd, onWM_HSCROLL, uiControl(s)); + uiSliderOnChanged(s, defaultOnChanged, NULL); + + SendMessageW(s->hwnd, TBM_SETRANGEMIN, (WPARAM) TRUE, (LPARAM) min); + SendMessageW(s->hwnd, TBM_SETRANGEMAX, (WPARAM) TRUE, (LPARAM) max); + SendMessageW(s->hwnd, TBM_SETPOS, (WPARAM) TRUE, (LPARAM) min); + + return s; +} diff --git a/src/libui_sdl/libui/windows/spinbox.cpp b/src/libui_sdl/libui/windows/spinbox.cpp new file mode 100644 index 0000000..2b6af66 --- /dev/null +++ b/src/libui_sdl/libui/windows/spinbox.cpp @@ -0,0 +1,215 @@ +// 8 april 2015 +#include "uipriv_windows.hpp" + +struct uiSpinbox { + uiWindowsControl c; + HWND hwnd; + HWND edit; + HWND updown; + void (*onChanged)(uiSpinbox *, void *); + void *onChangedData; + BOOL inhibitChanged; +}; + +// utility functions + +static int value(uiSpinbox *s) +{ + BOOL neededCap = FALSE; + LRESULT val; + + // This verifies the value put in, capping it automatically. + // We don't need to worry about checking for an error; that flag should really be called "did we have to cap?". + // We DO need to set the value in case of a cap though. + val = SendMessageW(s->updown, UDM_GETPOS32, 0, (LPARAM) (&neededCap)); + if (neededCap) { + s->inhibitChanged = TRUE; + SendMessageW(s->updown, UDM_SETPOS32, 0, (LPARAM) val); + s->inhibitChanged = FALSE; + } + return val; +} + +// control implementation + +static BOOL onWM_COMMAND(uiControl *c, HWND hwnd, WORD code, LRESULT *lResult) +{ + uiSpinbox *s = (uiSpinbox *) c; + WCHAR *wtext; + + if (code != EN_CHANGE) + return FALSE; + if (s->inhibitChanged) + return FALSE; + // We want to allow typing negative numbers; the natural way to do so is to start with a -. + // However, if we just have the code below, the up-down will catch the bare - and reject it. + // Let's fix that. + // This won't handle leading spaces, but spaces aren't allowed *anyway*. + wtext = windowText(s->edit); + if (wcscmp(wtext, L"-") == 0) { + uiFree(wtext); + return TRUE; + } + uiFree(wtext); + // value() does the work for us + value(s); + (*(s->onChanged))(s, s->onChangedData); + return TRUE; +} + +static void uiSpinboxDestroy(uiControl *c) +{ + uiSpinbox *s = uiSpinbox(c); + + uiWindowsUnregisterWM_COMMANDHandler(s->edit); + uiWindowsEnsureDestroyWindow(s->updown); + uiWindowsEnsureDestroyWindow(s->edit); + uiWindowsEnsureDestroyWindow(s->hwnd); + uiFreeControl(uiControl(s)); +} + +// TODO SyncEnableState +uiWindowsControlAllDefaultsExceptDestroy(uiSpinbox) + +// from http://msdn.microsoft.com/en-us/library/windows/desktop/dn742486.aspx#sizingandspacing +// TODO reduce this? +#define entryWidth 107 /* this is actually the shorter progress bar width, but Microsoft only indicates as wide as necessary */ +#define entryHeight 14 + +static void uiSpinboxMinimumSize(uiWindowsControl *c, int *width, int *height) +{ + uiSpinbox *s = uiSpinbox(c); + uiWindowsSizing sizing; + int x, y; + + x = entryWidth; + y = entryHeight; + // note that we go by the edit here + uiWindowsGetSizing(s->edit, &sizing); + uiWindowsSizingDlgUnitsToPixels(&sizing, &x, &y); + *width = x; + *height = y; +} + +static void spinboxArrangeChildren(uiSpinbox *s) +{ + LONG_PTR controlID; + HWND insertAfter; + + controlID = 100; + insertAfter = NULL; + uiWindowsEnsureAssignControlIDZOrder(s->edit, &controlID, &insertAfter); + uiWindowsEnsureAssignControlIDZOrder(s->updown, &controlID, &insertAfter); +} + +// an up-down control will only properly position itself the first time +// stupidly, there are no messages to force a size calculation, nor can I seem to reset the buddy window to force a new position +// alas, we have to make a new up/down control each time :( +static void recreateUpDown(uiSpinbox *s) +{ + BOOL preserve = FALSE; + int current; + // Microsoft's commctrl.h says to use this type + INT min, max; + + if (s->updown != NULL) { + preserve = TRUE; + current = value(s); + SendMessageW(s->updown, UDM_GETRANGE32, (WPARAM) (&min), (LPARAM) (&max)); + uiWindowsEnsureDestroyWindow(s->updown); + } + s->inhibitChanged = TRUE; + s->updown = CreateWindowExW(0, + UPDOWN_CLASSW, L"", + // no WS_VISIBLE; we set visibility ourselves + // up-down control should not be a tab stop + WS_CHILD | UDS_ALIGNRIGHT | UDS_ARROWKEYS | UDS_HOTTRACK | UDS_NOTHOUSANDS | UDS_SETBUDDYINT, + // this is important; it's necessary for autosizing to work + 0, 0, 0, 0, + s->hwnd, NULL, hInstance, NULL); + if (s->updown == NULL) + logLastError(L"error creating updown"); + SendMessageW(s->updown, UDM_SETBUDDY, (WPARAM) (s->edit), 0); + if (preserve) { + SendMessageW(s->updown, UDM_SETRANGE32, (WPARAM) min, (LPARAM) max); + SendMessageW(s->updown, UDM_SETPOS32, 0, (LPARAM) current); + } + // preserve the Z-order + spinboxArrangeChildren(s); + // TODO properly show/enable + ShowWindow(s->updown, SW_SHOW); + s->inhibitChanged = FALSE; +} + +static void spinboxRelayout(uiSpinbox *s) +{ + RECT r; + + // make the edit fill the container first; the new updown will resize it + uiWindowsEnsureGetClientRect(s->hwnd, &r); + uiWindowsEnsureMoveWindowDuringResize(s->edit, r.left, r.top, r.right - r.left, r.bottom - r.top); + recreateUpDown(s); +} + +static void defaultOnChanged(uiSpinbox *s, void *data) +{ + // do nothing +} + +int uiSpinboxValue(uiSpinbox *s) +{ + return value(s); +} + +void uiSpinboxSetValue(uiSpinbox *s, int value) +{ + s->inhibitChanged = TRUE; + SendMessageW(s->updown, UDM_SETPOS32, 0, (LPARAM) value); + s->inhibitChanged = FALSE; +} + +void uiSpinboxOnChanged(uiSpinbox *s, void (*f)(uiSpinbox *, void *), void *data) +{ + s->onChanged = f; + s->onChangedData = data; +} + +static void onResize(uiWindowsControl *c) +{ + spinboxRelayout(uiSpinbox(c)); +} + +uiSpinbox *uiNewSpinbox(int min, int max) +{ + uiSpinbox *s; + int temp; + + if (min >= max) { + temp = min; + min = max; + max = temp; + } + + uiWindowsNewControl(uiSpinbox, s); + + s->hwnd = uiWindowsMakeContainer(uiWindowsControl(s), onResize); + + s->edit = uiWindowsEnsureCreateControlHWND(WS_EX_CLIENTEDGE, + L"edit", L"", + // don't use ES_NUMBER; it doesn't allow typing in a leading - + ES_AUTOHSCROLL | ES_LEFT | ES_NOHIDESEL | WS_TABSTOP, + hInstance, NULL, + TRUE); + uiWindowsEnsureSetParentHWND(s->edit, s->hwnd); + + uiWindowsRegisterWM_COMMANDHandler(s->edit, onWM_COMMAND, uiControl(s)); + uiSpinboxOnChanged(s, defaultOnChanged, NULL); + + recreateUpDown(s); + s->inhibitChanged = TRUE; + SendMessageW(s->updown, UDM_SETRANGE32, (WPARAM) min, (LPARAM) max); + SendMessageW(s->updown, UDM_SETPOS32, 0, (LPARAM) min); + s->inhibitChanged = FALSE; + + return s; +} diff --git a/src/libui_sdl/libui/windows/stddialogs.cpp b/src/libui_sdl/libui/windows/stddialogs.cpp new file mode 100644 index 0000000..89d26ba --- /dev/null +++ b/src/libui_sdl/libui/windows/stddialogs.cpp @@ -0,0 +1,133 @@ +// 22 may 2015 +#include "uipriv_windows.hpp" + +// TODO document all this is what we want +// TODO do the same for font and color buttons + +// notes: +// - FOS_SUPPORTSTREAMABLEITEMS doesn't seem to be supported on windows vista, or at least not with the flags we use +// - even with FOS_NOVALIDATE the dialogs will reject invalid filenames (at least on Vista, anyway) +// - lack of FOS_NOREADONLYRETURN doesn't seem to matter on Windows 7 + +// TODO +// - http://blogs.msdn.com/b/wpfsdk/archive/2006/10/26/uncommon-dialogs--font-chooser-and-color-picker-dialogs.aspx +// - when a dialog is active, tab navigation in other windows stops working +// - when adding uiOpenFolder(), use IFileDialog as well - https://msdn.microsoft.com/en-us/library/windows/desktop/bb762115%28v=vs.85%29.aspx + +#define windowHWND(w) ((HWND) uiControlHandle(uiControl(w))) + +char *commonItemDialog(HWND parent, REFCLSID clsid, REFIID iid, FILEOPENDIALOGOPTIONS optsadd) +{ + IFileDialog *d = NULL; + FILEOPENDIALOGOPTIONS opts; + IShellItem *result = NULL; + WCHAR *wname = NULL; + char *name = NULL; + HRESULT hr; + + hr = CoCreateInstance(clsid, + NULL, CLSCTX_INPROC_SERVER, + iid, (LPVOID *) (&d)); + if (hr != S_OK) { + logHRESULT(L"error creating common item dialog", hr); + // always return NULL on error + goto out; + } + hr = d->GetOptions(&opts); + if (hr != S_OK) { + logHRESULT(L"error getting current options", hr); + goto out; + } + opts |= optsadd; + // the other platforms don't check read-only; we won't either + opts &= ~FOS_NOREADONLYRETURN; + hr = d->SetOptions(opts); + if (hr != S_OK) { + logHRESULT(L"error setting options", hr); + goto out; + } + hr = d->Show(parent); + if (hr == HRESULT_FROM_WIN32(ERROR_CANCELLED)) + // cancelled; return NULL like we have ready + goto out; + if (hr != S_OK) { + logHRESULT(L"error showing dialog", hr); + goto out; + } + hr = d->GetResult(&result); + if (hr != S_OK) { + logHRESULT(L"error getting dialog result", hr); + goto out; + } + hr = result->GetDisplayName(SIGDN_FILESYSPATH, &wname); + if (hr != S_OK) { + logHRESULT(L"error getting filename", hr); + goto out; + } + name = toUTF8(wname); + +out: + if (wname != NULL) + CoTaskMemFree(wname); + if (result != NULL) + result->Release(); + if (d != NULL) + d->Release(); + return name; +} + +char *uiOpenFile(uiWindow *parent) +{ + char *res; + + disableAllWindowsExcept(parent); + res = commonItemDialog(windowHWND(parent), + CLSID_FileOpenDialog, IID_IFileOpenDialog, + FOS_NOCHANGEDIR | FOS_ALLNONSTORAGEITEMS | FOS_NOVALIDATE | FOS_PATHMUSTEXIST | FOS_FILEMUSTEXIST | FOS_SHAREAWARE | FOS_NOTESTFILECREATE | FOS_NODEREFERENCELINKS | FOS_FORCESHOWHIDDEN | FOS_DEFAULTNOMINIMODE); + enableAllWindowsExcept(parent); + return res; +} + +char *uiSaveFile(uiWindow *parent) +{ + char *res; + + disableAllWindowsExcept(parent); + res = commonItemDialog(windowHWND(parent), + CLSID_FileSaveDialog, IID_IFileSaveDialog, + FOS_OVERWRITEPROMPT | FOS_NOCHANGEDIR | FOS_ALLNONSTORAGEITEMS | FOS_NOVALIDATE | FOS_SHAREAWARE | FOS_NOTESTFILECREATE | FOS_NODEREFERENCELINKS | FOS_FORCESHOWHIDDEN | FOS_DEFAULTNOMINIMODE); + enableAllWindowsExcept(parent); + return res; +} + +// TODO switch to TaskDialogIndirect()? + +static void msgbox(HWND parent, const char *title, const char *description, TASKDIALOG_COMMON_BUTTON_FLAGS buttons, PCWSTR icon) +{ + WCHAR *wtitle, *wdescription; + HRESULT hr; + + wtitle = toUTF16(title); + wdescription = toUTF16(description); + + hr = TaskDialog(parent, NULL, NULL, wtitle, wdescription, buttons, icon, NULL); + if (hr != S_OK) + logHRESULT(L"error showing task dialog", hr); + + uiFree(wdescription); + uiFree(wtitle); +} + +void uiMsgBox(uiWindow *parent, const char *title, const char *description) +{ + disableAllWindowsExcept(parent); + msgbox(windowHWND(parent), title, description, TDCBF_OK_BUTTON, NULL); + enableAllWindowsExcept(parent); +} + +void uiMsgBoxError(uiWindow *parent, const char *title, const char *description) +{ + disableAllWindowsExcept(parent); + msgbox(windowHWND(parent), title, description, TDCBF_OK_BUTTON, TD_ERROR_ICON); + enableAllWindowsExcept(parent); +} diff --git a/src/libui_sdl/libui/windows/tab.cpp b/src/libui_sdl/libui/windows/tab.cpp new file mode 100644 index 0000000..365f5a1 --- /dev/null +++ b/src/libui_sdl/libui/windows/tab.cpp @@ -0,0 +1,287 @@ +// 16 may 2015 +#include "uipriv_windows.hpp" + +// You don't add controls directly to a tab control on Windows; instead you make them siblings and swap between them on a TCN_SELCHANGING/TCN_SELCHANGE notification pair. +// In addition, you use dialogs because they can be textured properly; other controls cannot. (Things will look wrong if the tab background in the current theme is fancy if you just use the tab background by itself; see http://stackoverflow.com/questions/30087540/why-are-my-programss-tab-controls-rendering-their-background-in-a-blocky-way-b.) + +struct uiTab { + uiWindowsControl c; + HWND hwnd; // of the outer container + HWND tabHWND; // of the tab control itself + std::vector<struct tabPage *> *pages; + HWND parent; +}; + +// utility functions + +static LRESULT curpage(uiTab *t) +{ + return SendMessageW(t->tabHWND, TCM_GETCURSEL, 0, 0); +} + +static struct tabPage *tabPage(uiTab *t, int i) +{ + return (*(t->pages))[i]; +} + +static void tabPageRect(uiTab *t, RECT *r) +{ + // this rect needs to be in parent window coordinates, but TCM_ADJUSTRECT wants a window rect, which is screen coordinates + // because we have each page as a sibling of the tab, use the tab's own rect as the input rect + uiWindowsEnsureGetWindowRect(t->tabHWND, r); + SendMessageW(t->tabHWND, TCM_ADJUSTRECT, (WPARAM) FALSE, (LPARAM) r); + // and get it in terms of the container instead of the screen + mapWindowRect(NULL, t->hwnd, r); +} + +static void tabRelayout(uiTab *t) +{ + struct tabPage *page; + RECT r; + + // first move the tab control itself + uiWindowsEnsureGetClientRect(t->hwnd, &r); + uiWindowsEnsureMoveWindowDuringResize(t->tabHWND, r.left, r.top, r.right - r.left, r.bottom - r.top); + + // then the current page + if (t->pages->size() == 0) + return; + page = tabPage(t, curpage(t)); + tabPageRect(t, &r); + uiWindowsEnsureMoveWindowDuringResize(page->hwnd, r.left, r.top, r.right - r.left, r.bottom - r.top); +} + +static void showHidePage(uiTab *t, LRESULT which, int hide) +{ + struct tabPage *page; + + if (which == (LRESULT) (-1)) + return; + page = tabPage(t, which); + if (hide) + ShowWindow(page->hwnd, SW_HIDE); + else { + ShowWindow(page->hwnd, SW_SHOW); + // we only resize the current page, so we have to resize it; before we can do that, we need to make sure we are of the right size + uiWindowsControlMinimumSizeChanged(uiWindowsControl(t)); + } +} + +// control implementation + +static BOOL onWM_NOTIFY(uiControl *c, HWND hwnd, NMHDR *nm, LRESULT *lResult) +{ + uiTab *t = uiTab(c); + + if (nm->code != TCN_SELCHANGING && nm->code != TCN_SELCHANGE) + return FALSE; + showHidePage(t, curpage(t), nm->code == TCN_SELCHANGING); + *lResult = 0; + if (nm->code == TCN_SELCHANGING) + *lResult = FALSE; + return TRUE; +} + +static void uiTabDestroy(uiControl *c) +{ + uiTab *t = uiTab(c); + uiControl *child; + + for (struct tabPage *&page : *(t->pages)) { + child = page->child; + tabPageDestroy(page); + if (child != NULL) { + uiControlSetParent(child, NULL); + uiControlDestroy(child); + } + } + delete t->pages; + uiWindowsUnregisterWM_NOTIFYHandler(t->tabHWND); + uiWindowsEnsureDestroyWindow(t->tabHWND); + uiWindowsEnsureDestroyWindow(t->hwnd); + uiFreeControl(uiControl(t)); +} + +uiWindowsControlDefaultHandle(uiTab) +uiWindowsControlDefaultParent(uiTab) +uiWindowsControlDefaultSetParent(uiTab) +uiWindowsControlDefaultToplevel(uiTab) +uiWindowsControlDefaultVisible(uiTab) +uiWindowsControlDefaultShow(uiTab) +uiWindowsControlDefaultHide(uiTab) +uiWindowsControlDefaultEnabled(uiTab) +uiWindowsControlDefaultEnable(uiTab) +uiWindowsControlDefaultDisable(uiTab) + +static void uiTabSyncEnableState(uiWindowsControl *c, int enabled) +{ + uiTab *t = uiTab(c); + + if (uiWindowsShouldStopSyncEnableState(uiWindowsControl(t), enabled)) + return; + EnableWindow(t->tabHWND, enabled); + for (struct tabPage *&page : *(t->pages)) + if (page->child != NULL) + uiWindowsControlSyncEnableState(uiWindowsControl(page->child), enabled); +} + +uiWindowsControlDefaultSetParentHWND(uiTab) + +static void uiTabMinimumSize(uiWindowsControl *c, int *width, int *height) +{ + uiTab *t = uiTab(c); + int pagewid, pageht; + struct tabPage *page; + RECT r; + + // only consider the current page + pagewid = 0; + pageht = 0; + if (t->pages->size() != 0) { + page = tabPage(t, curpage(t)); + tabPageMinimumSize(page, &pagewid, &pageht); + } + + r.left = 0; + r.top = 0; + r.right = pagewid; + r.bottom = pageht; + // this also includes the tabs themselves + SendMessageW(t->tabHWND, TCM_ADJUSTRECT, (WPARAM) TRUE, (LPARAM) (&r)); + *width = r.right - r.left; + *height = r.bottom - r.top; +} + +static void uiTabMinimumSizeChanged(uiWindowsControl *c) +{ + uiTab *t = uiTab(c); + + if (uiWindowsControlTooSmall(uiWindowsControl(t))) { + uiWindowsControlContinueMinimumSizeChanged(uiWindowsControl(t)); + return; + } + tabRelayout(t); +} + +uiWindowsControlDefaultLayoutRect(uiTab) +uiWindowsControlDefaultAssignControlIDZOrder(uiTab) + +static void uiTabChildVisibilityChanged(uiWindowsControl *c) +{ + // TODO eliminate the redundancy + uiWindowsControlMinimumSizeChanged(c); +} + +static void tabArrangePages(uiTab *t) +{ + LONG_PTR controlID = 100; + HWND insertAfter = NULL; + + // TODO is this first or last? + uiWindowsEnsureAssignControlIDZOrder(t->tabHWND, &controlID, &insertAfter); + for (struct tabPage *&page : *(t->pages)) + uiWindowsEnsureAssignControlIDZOrder(page->hwnd, &controlID, &insertAfter); +} + +void uiTabAppend(uiTab *t, const char *name, uiControl *child) +{ + uiTabInsertAt(t, name, t->pages->size(), child); +} + +void uiTabInsertAt(uiTab *t, const char *name, int n, uiControl *child) +{ + struct tabPage *page; + LRESULT hide, show; + TCITEMW item; + WCHAR *wname; + + // see below + hide = curpage(t); + + if (child != NULL) + uiControlSetParent(child, uiControl(t)); + + page = newTabPage(child); + uiWindowsEnsureSetParentHWND(page->hwnd, t->hwnd); + t->pages->insert(t->pages->begin() + n, page); + tabArrangePages(t); + + ZeroMemory(&item, sizeof (TCITEMW)); + item.mask = TCIF_TEXT; + wname = toUTF16(name); + item.pszText = wname; + if (SendMessageW(t->tabHWND, TCM_INSERTITEM, (WPARAM) n, (LPARAM) (&item)) == (LRESULT) -1) + logLastError(L"error adding tab to uiTab"); + uiFree(wname); + + // we need to do this because adding the first tab doesn't send a TCN_SELCHANGE; it just shows the page + show = curpage(t); + if (show != hide) { + showHidePage(t, hide, 1); + showHidePage(t, show, 0); + } +} + +void uiTabDelete(uiTab *t, int n) +{ + struct tabPage *page; + + // first delete the tab from the tab control + // if this is the current tab, no tab will be selected, which is good + if (SendMessageW(t->tabHWND, TCM_DELETEITEM, (WPARAM) n, 0) == FALSE) + logLastError(L"error deleting uiTab tab"); + + // now delete the page itself + page = tabPage(t, n); + if (page->child != NULL) + uiControlSetParent(page->child, NULL); + tabPageDestroy(page); + t->pages->erase(t->pages->begin() + n); +} + +int uiTabNumPages(uiTab *t) +{ + return t->pages->size(); +} + +int uiTabMargined(uiTab *t, int n) +{ + return tabPage(t, n)->margined; +} + +void uiTabSetMargined(uiTab *t, int n, int margined) +{ + struct tabPage *page; + + page = tabPage(t, n); + page->margined = margined; + // even if the page doesn't have a child it might still have a new minimum size with margins; this is the easiest way to verify it + uiWindowsControlMinimumSizeChanged(uiWindowsControl(t)); +} + +static void onResize(uiWindowsControl *c) +{ + tabRelayout(uiTab(c)); +} + +uiTab *uiNewTab(void) +{ + uiTab *t; + + uiWindowsNewControl(uiTab, t); + + t->hwnd = uiWindowsMakeContainer(uiWindowsControl(t), onResize); + + t->tabHWND = uiWindowsEnsureCreateControlHWND(0, + WC_TABCONTROLW, L"", + TCS_TOOLTIPS | WS_TABSTOP, + hInstance, NULL, + TRUE); + uiWindowsEnsureSetParentHWND(t->tabHWND, t->hwnd); + + uiWindowsRegisterWM_NOTIFYHandler(t->tabHWND, onWM_NOTIFY, uiControl(t)); + + t->pages = new std::vector<struct tabPage *>; + + return t; +} diff --git a/src/libui_sdl/libui/windows/tabpage.cpp b/src/libui_sdl/libui/windows/tabpage.cpp new file mode 100644 index 0000000..5283ce7 --- /dev/null +++ b/src/libui_sdl/libui/windows/tabpage.cpp @@ -0,0 +1,131 @@ +// 30 may 2015 +#include "uipriv_windows.hpp" + +// TODO refine error handling + +// from http://msdn.microsoft.com/en-us/library/windows/desktop/bb226818%28v=vs.85%29.aspx +#define tabMargin 7 + +static void tabPageMargins(struct tabPage *tp, int *mx, int *my) +{ + uiWindowsSizing sizing; + + *mx = 0; + *my = 0; + if (!tp->margined) + return; + uiWindowsGetSizing(tp->hwnd, &sizing); + *mx = tabMargin; + *my = tabMargin; + uiWindowsSizingDlgUnitsToPixels(&sizing, mx, my); +} + +static void tabPageRelayout(struct tabPage *tp) +{ + RECT r; + int mx, my; + HWND child; + + if (tp->child == NULL) + return; + uiWindowsEnsureGetClientRect(tp->hwnd, &r); + tabPageMargins(tp, &mx, &my); + r.left += mx; + r.top += my; + r.right -= mx; + r.bottom -= my; + child = (HWND) uiControlHandle(tp->child); + uiWindowsEnsureMoveWindowDuringResize(child, r.left, r.top, r.right - r.left, r.bottom - r.top); +} + +// dummy dialog procedure; see below for details +// let's handle parent messages here to avoid needing to subclass +// TODO do we need to handle DM_GETDEFID/DM_SETDEFID here too because of the ES_WANTRETURN stuff at http://blogs.msdn.com/b/oldnewthing/archive/2007/08/20/4470527.aspx? what about multiple default buttons (TODO)? +// TODO we definitely need to do something about edit message handling; it does a fake close of our parent on pressing escape, causing uiWindow to stop responding to maximizes but still respond to events and then die horribly on destruction +static INT_PTR CALLBACK dlgproc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + struct tabPage *tp; + LRESULT lResult; + + if (uMsg == WM_INITDIALOG) { + tp = (struct tabPage *) lParam; + tp->hwnd = hwnd; + SetWindowLongPtrW(hwnd, DWLP_USER, (LONG_PTR) tp); + return TRUE; + } + if (handleParentMessages(hwnd, uMsg, wParam, lParam, &lResult) != FALSE) { + SetWindowLongPtrW(hwnd, DWLP_MSGRESULT, (LONG_PTR) lResult); + return TRUE; + } + if (uMsg == WM_WINDOWPOSCHANGED) { + tp = (struct tabPage *) GetWindowLongPtrW(hwnd, DWLP_USER); + tabPageRelayout(tp); + // pretend the dialog hasn't handled this just in case it needs to do something special + return FALSE; + } + + // unthemed dialogs don't respond to WM_PRINTCLIENT + // fortunately they don't have any special painting + if (uMsg == WM_PRINTCLIENT) { + // don't worry about the return value; hopefully DefWindowProcW() caught it (if not the dialog procedure itself) + // we COULD paint the dialog background brush ourselves but meh, it works + SendMessageW(hwnd, WM_ERASEBKGND, wParam, lParam); + // and pretend we did nothing just so the themed dialog can still paint its content + // TODO see if w ecan avoid erasing the background in this case in the first place, or if we even need to + return FALSE; + } + + return FALSE; +} + +struct tabPage *newTabPage(uiControl *child) +{ + struct tabPage *tp; + HRESULT hr; + + tp = uiNew(struct tabPage); + + // unfortunately this needs to be a proper dialog for EnableThemeDialogTexture() to work; CreateWindowExW() won't suffice + if (CreateDialogParamW(hInstance, MAKEINTRESOURCE(rcTabPageDialog), + utilWindow, dlgproc, (LPARAM) tp) == NULL) + logLastError(L"error creating tab page"); + + tp->child = child; + if (tp->child != NULL) { + uiWindowsEnsureSetParentHWND((HWND) uiControlHandle(tp->child), tp->hwnd); + uiWindowsControlAssignSoleControlIDZOrder(uiWindowsControl(tp->child)); + } + + hr = EnableThemeDialogTexture(tp->hwnd, ETDT_ENABLE | ETDT_USETABTEXTURE | ETDT_ENABLETAB); + if (hr != S_OK) + logHRESULT(L"error setting tab page background", hr); + // continue anyway; it'll look wrong but eh + + // and start the tab page hidden + ShowWindow(tp->hwnd, SW_HIDE); + + return tp; +} + +void tabPageDestroy(struct tabPage *tp) +{ + // don't destroy the child with the page + if (tp->child != NULL) + uiWindowsControlSetParentHWND(uiWindowsControl(tp->child), NULL); + // don't call EndDialog(); that's for the DialogBox() family of functions instead of CreateDialog() + uiWindowsEnsureDestroyWindow(tp->hwnd); + uiFree(tp); +} + +void tabPageMinimumSize(struct tabPage *tp, int *width, int *height) +{ + int mx, my; + + *width = 0; + *height = 0; + if (tp->child != NULL) + uiWindowsControlMinimumSize(uiWindowsControl(tp->child), width, height); + tabPageMargins(tp, &mx, &my); + *width += 2 * mx; + *height += 2 * my; +} diff --git a/src/libui_sdl/libui/windows/text.cpp b/src/libui_sdl/libui/windows/text.cpp new file mode 100644 index 0000000..af79fb8 --- /dev/null +++ b/src/libui_sdl/libui/windows/text.cpp @@ -0,0 +1,107 @@ +// 9 april 2015 +#include "uipriv_windows.hpp" + +WCHAR *windowTextAndLen(HWND hwnd, LRESULT *len) +{ + LRESULT n; + WCHAR *text; + + n = SendMessageW(hwnd, WM_GETTEXTLENGTH, 0, 0); + if (len != NULL) + *len = n; + // WM_GETTEXTLENGTH does not include the null terminator + text = (WCHAR *) uiAlloc((n + 1) * sizeof (WCHAR), "WCHAR[]"); + // note the comparison: the size includes the null terminator, but the return does not + if (GetWindowTextW(hwnd, text, n + 1) != n) { + logLastError(L"error getting window text"); + // on error, return an empty string to be safe + *text = L'\0'; + if (len != NULL) + *len = 0; + } + return text; +} + +WCHAR *windowText(HWND hwnd) +{ + return windowTextAndLen(hwnd, NULL); +} + +void setWindowText(HWND hwnd, WCHAR *wtext) +{ + if (SetWindowTextW(hwnd, wtext) == 0) + logLastError(L"error setting window text"); +} + +void uiFreeText(char *text) +{ + uiFree(text); +} + +int uiWindowsWindowTextWidth(HWND hwnd) +{ + LRESULT len; + WCHAR *text; + HDC dc; + HFONT prevfont; + SIZE size; + + size.cx = 0; + size.cy = 0; + + text = windowTextAndLen(hwnd, &len); + if (len == 0) // no text; nothing to do + goto noTextOrError; + + // now we can do the calculations + dc = GetDC(hwnd); + if (dc == NULL) { + logLastError(L"error getting DC"); + // on any error, assume no text + goto noTextOrError; + } + prevfont = (HFONT) SelectObject(dc, hMessageFont); + if (prevfont == NULL) { + logLastError(L"error loading control font into device context"); + ReleaseDC(hwnd, dc); + goto noTextOrError; + } + if (GetTextExtentPoint32W(dc, text, len, &size) == 0) { + logLastError(L"error getting text extent point"); + // continue anyway, assuming size is 0 + size.cx = 0; + size.cy = 0; + } + // continue on errors; we got what we want + if (SelectObject(dc, prevfont) != hMessageFont) + logLastError(L"error restoring previous font into device context"); + if (ReleaseDC(hwnd, dc) == 0) + logLastError(L"error releasing DC"); + + uiFree(text); + return size.cx; + +noTextOrError: + uiFree(text); + return 0; +} + +char *uiWindowsWindowText(HWND hwnd) +{ + WCHAR *wtext; + char *text; + + wtext = windowText(hwnd); + text = toUTF8(wtext); + uiFree(wtext); + return text; +} + +void uiWindowsSetWindowText(HWND hwnd, const char *text) +{ + WCHAR *wtext; + + wtext = toUTF16(text); + setWindowText(hwnd, wtext); + uiFree(wtext); +} diff --git a/src/libui_sdl/libui/windows/uipriv_windows.hpp b/src/libui_sdl/libui/windows/uipriv_windows.hpp new file mode 100644 index 0000000..6ffe09f --- /dev/null +++ b/src/libui_sdl/libui/windows/uipriv_windows.hpp @@ -0,0 +1,164 @@ +// 21 april 2016 +#include "winapi.hpp" +#include "../ui.h" +#include "../ui_windows.h" +#include "../common/uipriv.h" +#include "resources.hpp" +#include "compilerver.hpp" + +// ui internal window messages +enum { + // redirected WM_COMMAND and WM_NOTIFY + msgCOMMAND = WM_APP + 0x40, // start offset just to be safe + msgNOTIFY, + msgHSCROLL, + msgQueued, + msgD2DScratchPaint, + msgD2DScratchLButtonDown, +}; + +// alloc.cpp +extern void initAlloc(void); +extern void uninitAlloc(void); + +// events.cpp +extern BOOL runWM_COMMAND(WPARAM wParam, LPARAM lParam, LRESULT *lResult); +extern BOOL runWM_NOTIFY(WPARAM wParam, LPARAM lParam, LRESULT *lResult); +extern BOOL runWM_HSCROLL(WPARAM wParam, LPARAM lParam, LRESULT *lResult); +extern void issueWM_WININICHANGE(WPARAM wParam, LPARAM lParam); + +// utf16.cpp +#define emptyUTF16() ((WCHAR *) uiAlloc(1 * sizeof (WCHAR), "WCHAR[]")) +#define emptyUTF8() ((char *) uiAlloc(1 * sizeof (char), "char[]")) +extern WCHAR *toUTF16(const char *str); +extern char *toUTF8(const WCHAR *wstr); +extern WCHAR *utf16dup(const WCHAR *orig); +extern WCHAR *strf(const WCHAR *format, ...); +extern WCHAR *vstrf(const WCHAR *format, va_list ap); +extern char *LFtoCRLF(const char *lfonly); +extern void CRLFtoLF(char *s); +extern WCHAR *ftoutf16(double d); +extern WCHAR *itoutf16(int i); + +// debug.cpp +// see http://stackoverflow.com/questions/14421656/is-there-widely-available-wide-character-variant-of-file +// we turn __LINE__ into a string because PRIiMAX can't be converted to a wide string in MSVC (it seems to be defined as "ll" "i" according to the compiler errors) +// also note the use of __FUNCTION__ here; __func__ doesn't seem to work for some reason +#define _ws2(m) L ## m +#define _ws(m) _ws2(m) +#define _ws2n(m) L ## #m +#define _wsn(m) _ws2n(m) +#define debugargs const WCHAR *file, const WCHAR *line, const WCHAR *func +extern HRESULT _logLastError(debugargs, const WCHAR *s); +#ifdef _MSC_VER +#define logLastError(s) _logLastError(_ws(__FILE__), _wsn(__LINE__), _ws(__FUNCTION__), s) +#else +#define logLastError(s) _logLastError(_ws(__FILE__), _wsn(__LINE__), L"TODO none of the function name macros are macros in MinGW", s) +#endif +extern HRESULT _logHRESULT(debugargs, const WCHAR *s, HRESULT hr); +#ifdef _MSC_VER +#define logHRESULT(s, hr) _logHRESULT(_ws(__FILE__), _wsn(__LINE__), _ws(__FUNCTION__), s, hr) +#else +#define logHRESULT(s, hr) _logHRESULT(_ws(__FILE__), _wsn(__LINE__), L"TODO none of the function name macros are macros in MinGW", s, hr) +#endif + +// winutil.cpp +extern int windowClassOf(HWND hwnd, ...); +extern void mapWindowRect(HWND from, HWND to, RECT *r); +extern DWORD getStyle(HWND hwnd); +extern void setStyle(HWND hwnd, DWORD style); +extern DWORD getExStyle(HWND hwnd); +extern void setExStyle(HWND hwnd, DWORD exstyle); +extern void clientSizeToWindowSize(HWND hwnd, int *width, int *height, BOOL hasMenubar); +extern HWND parentOf(HWND child); +extern HWND parentToplevel(HWND child); +extern void setWindowInsertAfter(HWND hwnd, HWND insertAfter); +extern HWND getDlgItem(HWND hwnd, int id); +extern void invalidateRect(HWND hwnd, RECT *r, BOOL erase); + +// text.cpp +extern WCHAR *windowTextAndLen(HWND hwnd, LRESULT *len); +extern WCHAR *windowText(HWND hwnd); +extern void setWindowText(HWND hwnd, WCHAR *wtext); + +// init.cpp +extern HINSTANCE hInstance; +extern int nCmdShow; +extern HFONT hMessageFont; +extern HBRUSH hollowBrush; +extern uiInitOptions options; + +// utilwin.cpp +extern HWND utilWindow; +extern const char *initUtilWindow(HICON hDefaultIcon, HCURSOR hDefaultCursor); +extern void uninitUtilWindow(void); + +// main.cpp +extern int registerMessageFilter(void); +extern void unregisterMessageFilter(void); + +// parent.cpp +extern void paintContainerBackground(HWND hwnd, HDC dc, RECT *paintRect); +extern BOOL handleParentMessages(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT *lResult); + +// d2dscratch.cpp +extern ATOM registerD2DScratchClass(HICON hDefaultIcon, HCURSOR hDefaultCursor); +extern void unregisterD2DScratchClass(void); +extern HWND newD2DScratch(HWND parent, RECT *rect, HMENU controlID, SUBCLASSPROC subclass, DWORD_PTR subclassData); + +// area.cpp +#define areaClass L"libui_uiAreaClass" +extern ATOM registerAreaClass(HICON, HCURSOR); +extern void unregisterArea(void); + +// areaevents.cpp +extern BOOL areaFilter(MSG *); + +// window.cpp +extern ATOM registerWindowClass(HICON, HCURSOR); +extern void unregisterWindowClass(void); +extern void ensureMinimumWindowSize(uiWindow *); +extern void disableAllWindowsExcept(uiWindow *which); +extern void enableAllWindowsExcept(uiWindow *which); + +// container.cpp +#define containerClass L"libui_uiContainerClass" +extern ATOM initContainer(HICON, HCURSOR); +extern void uninitContainer(void); + +// tabpage.cpp +struct tabPage { + HWND hwnd; + uiControl *child; + BOOL margined; +}; +extern struct tabPage *newTabPage(uiControl *child); +extern void tabPageDestroy(struct tabPage *tp); +extern void tabPageMinimumSize(struct tabPage *tp, int *width, int *height); + +// colordialog.cpp +struct colorDialogRGBA { + double r; + double g; + double b; + double a; +}; +extern BOOL showColorDialog(HWND parent, struct colorDialogRGBA *c); + +// sizing.cpp +extern void getSizing(HWND hwnd, uiWindowsSizing *sizing, HFONT font); + +// graphemes.cpp +extern size_t *graphemes(WCHAR *msg); + +// TODO move into a dedicated file abibugs.cpp when we rewrite the drawing code +extern D2D1_SIZE_F realGetSize(ID2D1RenderTarget *rt); + + + + +// TODO +#include "_uipriv_migrate.hpp" + +// draw.cpp +extern ID2D1DCRenderTarget *makeHDCRenderTarget(HDC dc, RECT *r); diff --git a/src/libui_sdl/libui/windows/utf16.cpp b/src/libui_sdl/libui/windows/utf16.cpp new file mode 100644 index 0000000..98954d0 --- /dev/null +++ b/src/libui_sdl/libui/windows/utf16.cpp @@ -0,0 +1,153 @@ +// 21 april 2016 +#include "uipriv_windows.hpp" + +// see http://stackoverflow.com/a/29556509/3408572 + +#define MBTWC(str, wstr, bufsiz) MultiByteToWideChar(CP_UTF8, 0, str, -1, wstr, bufsiz) + +WCHAR *toUTF16(const char *str) +{ + WCHAR *wstr; + int n; + + if (*str == '\0') // empty string + return emptyUTF16(); + n = MBTWC(str, NULL, 0); + if (n == 0) { + logLastError(L"error figuring out number of characters to convert to"); + return emptyUTF16(); + } + wstr = (WCHAR *) uiAlloc(n * sizeof (WCHAR), "WCHAR[]"); + if (MBTWC(str, wstr, n) != n) { + logLastError(L"error converting from UTF-8 to UTF-16"); + // and return an empty string + *wstr = L'\0'; + } + return wstr; +} + +#define WCTMB(wstr, str, bufsiz) WideCharToMultiByte(CP_UTF8, 0, wstr, -1, str, bufsiz, NULL, NULL) + +char *toUTF8(const WCHAR *wstr) +{ + char *str; + int n; + + if (*wstr == L'\0') // empty string + return emptyUTF8(); + n = WCTMB(wstr, NULL, 0); + if (n == 0) { + logLastError(L"error figuring out number of characters to convert to"); + return emptyUTF8(); + } + str = (char *) uiAlloc(n * sizeof (char), "char[]"); + if (WCTMB(wstr, str, n) != n) { + logLastError(L"error converting from UTF-16 to UTF-8"); + // and return an empty string + *str = '\0'; + } + return str; +} + +WCHAR *utf16dup(const WCHAR *orig) +{ + WCHAR *out; + size_t len; + + len = wcslen(orig); + out = (WCHAR *) uiAlloc((len + 1) * sizeof (WCHAR), "WCHAR[]"); + wcscpy_s(out, len + 1, orig); + return out; +} + +WCHAR *strf(const WCHAR *format, ...) +{ + va_list ap; + WCHAR *str; + + va_start(ap, format); + str = vstrf(format, ap); + va_end(ap); + return str; +} + +WCHAR *vstrf(const WCHAR *format, va_list ap) +{ + va_list ap2; + WCHAR *buf; + size_t n; + + if (*format == L'\0') + return emptyUTF16(); + + va_copy(ap2, ap); + n = _vscwprintf(format, ap2); + va_end(ap2); + n++; // terminating L'\0' + + buf = (WCHAR *) uiAlloc(n * sizeof (WCHAR), "WCHAR[]"); + // includes terminating L'\0' according to example in https://msdn.microsoft.com/en-us/library/xa1a1a6z.aspx + vswprintf_s(buf, n, format, ap); + + return buf; +} + +// Let's shove these utility routines here too. +// Prerequisite: lfonly is UTF-8. +char *LFtoCRLF(const char *lfonly) +{ + char *crlf; + size_t i, len; + char *out; + + len = strlen(lfonly); + crlf = (char *) uiAlloc((len * 2 + 1) * sizeof (char), "char[]"); + out = crlf; + for (i = 0; i < len; i++) { + if (*lfonly == '\n') + *crlf++ = '\r'; + *crlf++ = *lfonly++; + } + *crlf = '\0'; + return out; +} + +// Prerequisite: s is UTF-8. +void CRLFtoLF(char *s) +{ + char *t = s; + + for (; *s != '\0'; s++) { + // be sure to preserve \rs that are genuinely there + if (*s == '\r' && *(s + 1) == '\n') + continue; + *t++ = *s; + } + *t = '\0'; + // pad out the rest of t, just to be safe + while (t != s) + *t++ = '\0'; +} + +// std::to_string() always uses %f; we want %g +// fortunately std::iostream seems to use %g by default so +WCHAR *ftoutf16(double d) +{ + std::wostringstream ss; + std::wstring s; + + ss << d; + s = ss.str(); // to be safe + return utf16dup(s.c_str()); +} + +// to complement the above +WCHAR *itoutf16(int i) +{ + std::wostringstream ss; + std::wstring s; + + ss << i; + s = ss.str(); // to be safe + return utf16dup(s.c_str()); +} diff --git a/src/libui_sdl/libui/windows/utilwin.cpp b/src/libui_sdl/libui/windows/utilwin.cpp new file mode 100644 index 0000000..414ae83 --- /dev/null +++ b/src/libui_sdl/libui/windows/utilwin.cpp @@ -0,0 +1,76 @@ +// 14 may 2015 +#include "uipriv_windows.hpp" + +// The utility window is a special window that performs certain tasks internal to libui. +// It is not a message-only window, and it is always hidden and disabled. +// Its roles: +// - It is the initial parent of all controls. When a control loses its parent, it also becomes that control's parent. +// - It handles WM_QUERYENDSESSION and console end session requests. +// - It handles WM_WININICHANGE and forwards the message to any child windows that request it. +// - It handles executing functions queued to run by uiQueueMain(). + +#define utilWindowClass L"libui_utilWindowClass" + +HWND utilWindow; + +static LRESULT CALLBACK utilWindowWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + void (*qf)(void *); + LRESULT lResult; + + if (handleParentMessages(hwnd, uMsg, wParam, lParam, &lResult) != FALSE) + return lResult; + switch (uMsg) { + case WM_QUERYENDSESSION: + // TODO block handler + if (shouldQuit()) { + uiQuit(); + return TRUE; + } + return FALSE; + case WM_WININICHANGE: + issueWM_WININICHANGE(wParam, lParam); + return 0; + case msgQueued: + qf = (void (*)(void *)) wParam; + (*qf)((void *) lParam); + return 0; + } + return DefWindowProcW(hwnd, uMsg, wParam, lParam); +} + +const char *initUtilWindow(HICON hDefaultIcon, HCURSOR hDefaultCursor) +{ + WNDCLASSW wc; + + ZeroMemory(&wc, sizeof (WNDCLASSW)); + wc.lpszClassName = utilWindowClass; + wc.lpfnWndProc = utilWindowWndProc; + wc.hInstance = hInstance; + wc.hIcon = hDefaultIcon; + wc.hCursor = hDefaultCursor; + wc.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1); + if (RegisterClass(&wc) == 0) + // see init.cpp for an explanation of the =s + return "=registering utility window class"; + + utilWindow = CreateWindowExW(0, + utilWindowClass, L"libui utility window", + WS_OVERLAPPEDWINDOW, + 0, 0, 100, 100, + NULL, NULL, hInstance, NULL); + if (utilWindow == NULL) + return "=creating utility window"; + // and just to be safe + EnableWindow(utilWindow, FALSE); + + return NULL; +} + +void uninitUtilWindow(void) +{ + if (DestroyWindow(utilWindow) == 0) + logLastError(L"error destroying utility window"); + if (UnregisterClass(utilWindowClass, hInstance) == 0) + logLastError(L"error unregistering utility window class"); +} diff --git a/src/libui_sdl/libui/windows/winapi.hpp b/src/libui_sdl/libui/windows/winapi.hpp new file mode 100644 index 0000000..86aba5d --- /dev/null +++ b/src/libui_sdl/libui/windows/winapi.hpp @@ -0,0 +1,51 @@ +// 31 may 2015 +#define UNICODE +#define _UNICODE +#define STRICT +#define STRICT_TYPED_ITEMIDS + +// see https://github.com/golang/go/issues/9916#issuecomment-74812211 +// TODO get rid of this +#define INITGUID + +// for the manifest +#ifndef _UI_STATIC +#define ISOLATION_AWARE_ENABLED 1 +#endif + +// get Windows version right; right now Windows Vista +// unless otherwise stated, all values from Microsoft's sdkddkver.h +// TODO is all of this necessary? how is NTDDI_VERSION used? +// TODO plaform update sp2 +#define WINVER 0x0600 /* from Microsoft's winnls.h */ +#define _WIN32_WINNT 0x0600 +#define _WIN32_WINDOWS 0x0600 /* from Microsoft's pdh.h */ +#define _WIN32_IE 0x0700 +#define NTDDI_VERSION 0x06000000 + +#include <windows.h> + +// Microsoft's resource compiler will segfault if we feed it headers it was not designed to handle +#ifndef RC_INVOKED +#include <commctrl.h> +#include <uxtheme.h> +#include <windowsx.h> +#include <shobjidl.h> +#include <d2d1.h> +#include <d2d1helper.h> +#include <dwrite.h> +#include <usp10.h> + +#include <stdint.h> +#include <string.h> +#include <wchar.h> +#include <stdarg.h> +#include <stdio.h> +#include <math.h> +#include <float.h> +#include <inttypes.h> + +#include <vector> +#include <map> +#include <sstream> +#endif diff --git a/src/libui_sdl/libui/windows/window.cpp b/src/libui_sdl/libui/windows/window.cpp new file mode 100644 index 0000000..9cf13a2 --- /dev/null +++ b/src/libui_sdl/libui/windows/window.cpp @@ -0,0 +1,533 @@ +// 27 april 2015 +#include "uipriv_windows.hpp" + +#define windowClass L"libui_uiWindowClass" + +struct uiWindow { + uiWindowsControl c; + HWND hwnd; + HMENU menubar; + uiControl *child; + BOOL shownOnce; + int visible; + int (*onClosing)(uiWindow *, void *); + void *onClosingData; + int margined; + BOOL hasMenubar; + void (*onContentSizeChanged)(uiWindow *, void *); + void *onContentSizeChangedData; + BOOL changingSize; + int fullscreen; + WINDOWPLACEMENT fsPrevPlacement; + int borderless; +}; + +// from https://msdn.microsoft.com/en-us/library/windows/desktop/dn742486.aspx#sizingandspacing +#define windowMargin 7 + +static void windowMargins(uiWindow *w, int *mx, int *my) +{ + uiWindowsSizing sizing; + + *mx = 0; + *my = 0; + if (!w->margined) + return; + uiWindowsGetSizing(w->hwnd, &sizing); + *mx = windowMargin; + *my = windowMargin; + uiWindowsSizingDlgUnitsToPixels(&sizing, mx, my); +} + +static void windowRelayout(uiWindow *w) +{ + int x, y, width, height; + RECT r; + int mx, my; + HWND child; + + if (w->child == NULL) + return; + x = 0; + y = 0; + uiWindowsEnsureGetClientRect(w->hwnd, &r); + width = r.right - r.left; + height = r.bottom - r.top; + windowMargins(w, &mx, &my); + x += mx; + y += my; + width -= 2 * mx; + height -= 2 * my; + child = (HWND) uiControlHandle(w->child); + uiWindowsEnsureMoveWindowDuringResize(child, x, y, width, height); +} + +static LRESULT CALLBACK windowWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + LONG_PTR ww; + uiWindow *w; + CREATESTRUCTW *cs = (CREATESTRUCTW *) lParam; + WINDOWPOS *wp = (WINDOWPOS *) lParam; + MINMAXINFO *mmi = (MINMAXINFO *) lParam; + int width, height; + LRESULT lResult; + + ww = GetWindowLongPtrW(hwnd, GWLP_USERDATA); + if (ww == 0) { + if (uMsg == WM_CREATE) + SetWindowLongPtrW(hwnd, GWLP_USERDATA, (LONG_PTR) (cs->lpCreateParams)); + // fall through to DefWindowProc() anyway + return DefWindowProcW(hwnd, uMsg, wParam, lParam); + } + w = uiWindow((void *) ww); + if (handleParentMessages(hwnd, uMsg, wParam, lParam, &lResult) != FALSE) + return lResult; + switch (uMsg) { + case WM_COMMAND: + // not a menu + if (lParam != 0) + break; + if (HIWORD(wParam) != 0) + break; + runMenuEvent(LOWORD(wParam), uiWindow(w)); + return 0; + case WM_WINDOWPOSCHANGED: + if ((wp->flags & SWP_NOSIZE) != 0) + break; + if (w->onContentSizeChanged != NULL) // TODO figure out why this is happening too early + if (!w->changingSize) + (*(w->onContentSizeChanged))(w, w->onContentSizeChangedData); + windowRelayout(w); + return 0; + case WM_GETMINMAXINFO: + // ensure the user cannot resize the window smaller than its minimum size + lResult = DefWindowProcW(hwnd, uMsg, wParam, lParam); + uiWindowsControlMinimumSize(uiWindowsControl(w), &width, &height); + // width and height are in client coordinates; ptMinTrackSize is in window coordinates + clientSizeToWindowSize(w->hwnd, &width, &height, w->hasMenubar); + mmi->ptMinTrackSize.x = width; + mmi->ptMinTrackSize.y = height; + return lResult; + case WM_PRINTCLIENT: + // we do no special painting; just erase the background + // don't worry about the return value; we let DefWindowProcW() handle this message + SendMessageW(hwnd, WM_ERASEBKGND, wParam, lParam); + return 0; + case WM_CLOSE: + if ((*(w->onClosing))(w, w->onClosingData)) + uiControlDestroy(uiControl(w)); + return 0; // we destroyed it already + } + return DefWindowProcW(hwnd, uMsg, wParam, lParam); +} + +ATOM registerWindowClass(HICON hDefaultIcon, HCURSOR hDefaultCursor) +{ + WNDCLASSW wc; + + ZeroMemory(&wc, sizeof (WNDCLASSW)); + wc.lpszClassName = windowClass; + wc.lpfnWndProc = windowWndProc; + wc.hInstance = hInstance; + wc.hIcon = hDefaultIcon; + wc.hCursor = hDefaultCursor; + wc.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1); + return RegisterClassW(&wc); +} + +void unregisterWindowClass(void) +{ + if (UnregisterClassW(windowClass, hInstance) == 0) + logLastError(L"error unregistering uiWindow window class"); +} + +static int defaultOnClosing(uiWindow *w, void *data) +{ + return 0; +} + +static void defaultOnPositionContentSizeChanged(uiWindow *w, void *data) +{ + // do nothing +} + +static std::map<uiWindow *, bool> windows; + +static void uiWindowDestroy(uiControl *c) +{ + uiWindow *w = uiWindow(c); + + // first hide ourselves + ShowWindow(w->hwnd, SW_HIDE); + // now destroy the child + if (w->child != NULL) { + uiControlSetParent(w->child, NULL); + uiControlDestroy(w->child); + } + // now free the menubar, if any + if (w->menubar != NULL) + freeMenubar(w->menubar); + // and finally free ourselves + windows.erase(w); + uiWindowsEnsureDestroyWindow(w->hwnd); + uiFreeControl(uiControl(w)); +} + +uiWindowsControlDefaultHandle(uiWindow) + +uiControl *uiWindowParent(uiControl *c) +{ + return NULL; +} + +void uiWindowSetParent(uiControl *c, uiControl *parent) +{ + uiUserBugCannotSetParentOnToplevel("uiWindow"); +} + +static int uiWindowToplevel(uiControl *c) +{ + return 1; +} + +// TODO initial state of windows is hidden; ensure this here and make it so on other platforms +static int uiWindowVisible(uiControl *c) +{ + uiWindow *w = uiWindow(c); + + return w->visible; +} + +static void uiWindowShow(uiControl *c) +{ + uiWindow *w = uiWindow(c); + + w->visible = 1; + // just in case the window's minimum size wasn't recalculated already + ensureMinimumWindowSize(w); + if (w->shownOnce) { + ShowWindow(w->hwnd, SW_SHOW); + return; + } + w->shownOnce = TRUE; + // make sure the child is the correct size + uiWindowsControlMinimumSizeChanged(uiWindowsControl(w)); + ShowWindow(w->hwnd, nCmdShow); + if (UpdateWindow(w->hwnd) == 0) + logLastError(L"error calling UpdateWindow() after showing uiWindow for the first time"); +} + +static void uiWindowHide(uiControl *c) +{ + uiWindow *w = uiWindow(c); + + w->visible = 0; + ShowWindow(w->hwnd, SW_HIDE); +} + +// TODO we don't want the window to be disabled completely; that would prevent it from being moved! ...would it? +uiWindowsControlDefaultEnabled(uiWindow) +uiWindowsControlDefaultEnable(uiWindow) +uiWindowsControlDefaultDisable(uiWindow) +// TODO we need to do something about undocumented fields in the OS control types +uiWindowsControlDefaultSyncEnableState(uiWindow) +// TODO +uiWindowsControlDefaultSetParentHWND(uiWindow) + +static void uiWindowMinimumSize(uiWindowsControl *c, int *width, int *height) +{ + uiWindow *w = uiWindow(c); + int mx, my; + + *width = 0; + *height = 0; + if (w->child != NULL) + uiWindowsControlMinimumSize(uiWindowsControl(w->child), width, height); + windowMargins(w, &mx, &my); + *width += 2 * mx; + *height += 2 * my; +} + +static void uiWindowMinimumSizeChanged(uiWindowsControl *c) +{ + uiWindow *w = uiWindow(c); + + if (uiWindowsControlTooSmall(uiWindowsControl(w))) { + // TODO figure out what to do with this function + // maybe split it into two so WM_GETMINMAXINFO can use it? + ensureMinimumWindowSize(w); + return; + } + // otherwise we only need to re-layout everything + windowRelayout(w); +} + +static void uiWindowLayoutRect(uiWindowsControl *c, RECT *r) +{ + uiWindow *w = uiWindow(c); + + // the layout rect is the client rect in this case + uiWindowsEnsureGetClientRect(w->hwnd, r); +} + +uiWindowsControlDefaultAssignControlIDZOrder(uiWindow) + +static void uiWindowChildVisibilityChanged(uiWindowsControl *c) +{ + // TODO eliminate the redundancy + uiWindowsControlMinimumSizeChanged(c); +} + +char *uiWindowTitle(uiWindow *w) +{ + return uiWindowsWindowText(w->hwnd); +} + +void uiWindowSetTitle(uiWindow *w, const char *title) +{ + uiWindowsSetWindowText(w->hwnd, title); + // don't queue resize; the caption isn't part of what affects layout and sizing of the client area (it'll be ellipsized if too long) +} + +// this is used for both fullscreening and centering +// see also https://blogs.msdn.microsoft.com/oldnewthing/20100412-00/?p=14353 and https://blogs.msdn.microsoft.com/oldnewthing/20050505-04/?p=35703 +static void windowMonitorRect(HWND hwnd, RECT *r) +{ + HMONITOR monitor; + MONITORINFO mi; + + monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY); + ZeroMemory(&mi, sizeof (MONITORINFO)); + mi.cbSize = sizeof (MONITORINFO); + if (GetMonitorInfoW(monitor, &mi) == 0) { + logLastError(L"error getting window monitor rect"); + // default to SM_CXSCREEN x SM_CYSCREEN to be safe + r->left = 0; + r->top = 0; + r->right = GetSystemMetrics(SM_CXSCREEN); + r->bottom = GetSystemMetrics(SM_CYSCREEN); + return; + } + *r = mi.rcMonitor; +} + +void uiWindowContentSize(uiWindow *w, int *width, int *height) +{ + RECT r; + + uiWindowsEnsureGetClientRect(w->hwnd, &r); + *width = r.right - r.left; + *height = r.bottom - r.top; +} + +// TODO should this disallow too small? +void uiWindowSetContentSize(uiWindow *w, int width, int height) +{ + w->changingSize = TRUE; + clientSizeToWindowSize(w->hwnd, &width, &height, w->hasMenubar); + if (SetWindowPos(w->hwnd, NULL, 0, 0, width, height, SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOOWNERZORDER | SWP_NOZORDER) == 0) + logLastError(L"error resizing window"); + w->changingSize = FALSE; +} + +int uiWindowFullscreen(uiWindow *w) +{ + return w->fullscreen; +} + +void uiWindowSetFullscreen(uiWindow *w, int fullscreen) +{ + RECT r; + + if (w->fullscreen && fullscreen) + return; + if (!w->fullscreen && !fullscreen) + return; + w->fullscreen = fullscreen; + w->changingSize = TRUE; + if (w->fullscreen) { + ZeroMemory(&(w->fsPrevPlacement), sizeof (WINDOWPLACEMENT)); + w->fsPrevPlacement.length = sizeof (WINDOWPLACEMENT); + if (GetWindowPlacement(w->hwnd, &(w->fsPrevPlacement)) == 0) + logLastError(L"error getting old window placement"); + windowMonitorRect(w->hwnd, &r); + setStyle(w->hwnd, getStyle(w->hwnd) & ~WS_OVERLAPPEDWINDOW); + if (SetWindowPos(w->hwnd, HWND_TOP, + r.left, r.top, + r.right - r.left, r.bottom - r.top, + SWP_FRAMECHANGED | SWP_NOOWNERZORDER) == 0) + logLastError(L"error making window fullscreen"); + } else { + if (!w->borderless) // keep borderless until that is turned off + setStyle(w->hwnd, getStyle(w->hwnd) | WS_OVERLAPPEDWINDOW); + if (SetWindowPlacement(w->hwnd, &(w->fsPrevPlacement)) == 0) + logLastError(L"error leaving fullscreen"); + if (SetWindowPos(w->hwnd, NULL, + 0, 0, 0, 0, + SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOOWNERZORDER | SWP_NOSIZE | SWP_NOZORDER) == 0) + logLastError(L"error restoring window border after fullscreen"); + } + w->changingSize = FALSE; +} + +void uiWindowOnContentSizeChanged(uiWindow *w, void (*f)(uiWindow *, void *), void *data) +{ + w->onContentSizeChanged = f; + w->onContentSizeChangedData = data; +} + +void uiWindowOnClosing(uiWindow *w, int (*f)(uiWindow *, void *), void *data) +{ + w->onClosing = f; + w->onClosingData = data; +} + +int uiWindowBorderless(uiWindow *w) +{ + return w->borderless; +} + +// TODO window should move to the old client position and should not have the extra space the borders left behind +// TODO extract the relevant styles from WS_OVERLAPPEDWINDOW? +void uiWindowSetBorderless(uiWindow *w, int borderless) +{ + w->borderless = borderless; + if (w->borderless) + setStyle(w->hwnd, getStyle(w->hwnd) & ~WS_OVERLAPPEDWINDOW); + else + if (!w->fullscreen) // keep borderless until leaving fullscreen + setStyle(w->hwnd, getStyle(w->hwnd) | WS_OVERLAPPEDWINDOW); +} + +void uiWindowSetChild(uiWindow *w, uiControl *child) +{ + if (w->child != NULL) { + uiControlSetParent(w->child, NULL); + uiWindowsControlSetParentHWND(uiWindowsControl(w->child), NULL); + } + w->child = child; + if (w->child != NULL) { + uiControlSetParent(w->child, uiControl(w)); + uiWindowsControlSetParentHWND(uiWindowsControl(w->child), w->hwnd); + uiWindowsControlAssignSoleControlIDZOrder(uiWindowsControl(w->child)); + windowRelayout(w); + } +} + +int uiWindowMargined(uiWindow *w) +{ + return w->margined; +} + +void uiWindowSetMargined(uiWindow *w, int margined) +{ + w->margined = margined; + windowRelayout(w); +} + +// see http://blogs.msdn.com/b/oldnewthing/archive/2003/09/11/54885.aspx and http://blogs.msdn.com/b/oldnewthing/archive/2003/09/13/54917.aspx +// TODO use clientSizeToWindowSize() +static void setClientSize(uiWindow *w, int width, int height, BOOL hasMenubar, DWORD style, DWORD exstyle) +{ + RECT window; + + window.left = 0; + window.top = 0; + window.right = width; + window.bottom = height; + if (AdjustWindowRectEx(&window, style, hasMenubar, exstyle) == 0) + logLastError(L"error getting real window coordinates"); + if (hasMenubar) { + RECT temp; + + temp = window; + temp.bottom = 0x7FFF; // infinite height + SendMessageW(w->hwnd, WM_NCCALCSIZE, (WPARAM) FALSE, (LPARAM) (&temp)); + window.bottom += temp.top; + } + if (SetWindowPos(w->hwnd, NULL, 0, 0, window.right - window.left, window.bottom - window.top, SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOOWNERZORDER | SWP_NOZORDER) == 0) + logLastError(L"error resizing window"); +} + +uiWindow *uiNewWindow(const char *title, int width, int height, int hasMenubar) +{ + uiWindow *w; + WCHAR *wtitle; + BOOL hasMenubarBOOL; + + uiWindowsNewControl(uiWindow, w); + + hasMenubarBOOL = FALSE; + if (hasMenubar) + hasMenubarBOOL = TRUE; + w->hasMenubar = hasMenubarBOOL; + +#define style WS_OVERLAPPEDWINDOW +#define exstyle 0 + + wtitle = toUTF16(title); + w->hwnd = CreateWindowExW(exstyle, + windowClass, wtitle, + style, + CW_USEDEFAULT, CW_USEDEFAULT, + // use the raw width and height for now + // this will get CW_USEDEFAULT (hopefully) predicting well + // even if it doesn't, we're adjusting it later + width, height, + NULL, NULL, hInstance, w); + if (w->hwnd == NULL) + logLastError(L"error creating window"); + uiFree(wtitle); + + if (hasMenubar) { + w->menubar = makeMenubar(); + if (SetMenu(w->hwnd, w->menubar) == 0) + logLastError(L"error giving menu to window"); + } + + // and use the proper size + setClientSize(w, width, height, hasMenubarBOOL, style, exstyle); + + uiWindowOnClosing(w, defaultOnClosing, NULL); + uiWindowOnContentSizeChanged(w, defaultOnPositionContentSizeChanged, NULL); + + windows[w] = true; + return w; +} + +// this cannot queue a resize because it's called by the resize handler +void ensureMinimumWindowSize(uiWindow *w) +{ + int width, height; + RECT r; + + uiWindowsControlMinimumSize(uiWindowsControl(w), &width, &height); + uiWindowsEnsureGetClientRect(w->hwnd, &r); + if (width < (r.right - r.left)) // preserve width if larger + width = r.right - r.left; + if (height < (r.bottom - r.top)) // preserve height if larger + height = r.bottom - r.top; + clientSizeToWindowSize(w->hwnd, &width, &height, w->hasMenubar); + if (SetWindowPos(w->hwnd, NULL, 0, 0, width, height, SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOOWNERZORDER | SWP_NOZORDER) == 0) + logLastError(L"error resizing window"); +} + +void disableAllWindowsExcept(uiWindow *which) +{ + for (auto &w : windows) { + if (w.first == which) + continue; + EnableWindow(w.first->hwnd, FALSE); + } +} + +void enableAllWindowsExcept(uiWindow *which) +{ + for (auto &w : windows) { + if (w.first == which) + continue; + if (!uiControlEnabled(uiControl(w.first))) + continue; + EnableWindow(w.first->hwnd, TRUE); + } +} diff --git a/src/libui_sdl/libui/windows/winpublic.cpp b/src/libui_sdl/libui/windows/winpublic.cpp new file mode 100644 index 0000000..397a3b5 --- /dev/null +++ b/src/libui_sdl/libui/windows/winpublic.cpp @@ -0,0 +1,61 @@ +// 6 april 2015 +#include "uipriv_windows.hpp" + +void uiWindowsEnsureDestroyWindow(HWND hwnd) +{ + if (DestroyWindow(hwnd) == 0) + logLastError(L"error destroying window"); +} + +void uiWindowsEnsureSetParentHWND(HWND hwnd, HWND parent) +{ + if (parent == NULL) + parent = utilWindow; + if (SetParent(hwnd, parent) == 0) + logLastError(L"error setting window parent"); +} + +void uiWindowsEnsureAssignControlIDZOrder(HWND hwnd, LONG_PTR *controlID, HWND *insertAfter) +{ + SetWindowLongPtrW(hwnd, GWLP_ID, *controlID); + (*controlID)++; + setWindowInsertAfter(hwnd, *insertAfter); + *insertAfter = hwnd; +} + +void uiWindowsEnsureMoveWindowDuringResize(HWND hwnd, int x, int y, int width, int height) +{ + RECT r; + + r.left = x; + r.top = y; + r.right = x + width; + r.bottom = y + height; + if (SetWindowPos(hwnd, NULL, r.left, r.top, r.right - r.left, r.bottom - r.top, SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOZORDER) == 0) + logLastError(L"error moving window"); +} + +// do these function even error out in any case other than invalid parameters?! I thought all windows had rects +void uiWindowsEnsureGetClientRect(HWND hwnd, RECT *r) +{ + if (GetClientRect(hwnd, r) == 0) { + logLastError(L"error getting window client rect"); + // zero out the rect on error just to be safe + r->left = 0; + r->top = 0; + r->right = 0; + r->bottom = 0; + } +} + +void uiWindowsEnsureGetWindowRect(HWND hwnd, RECT *r) +{ + if (GetWindowRect(hwnd, r) == 0) { + logLastError(L"error getting window rect"); + // zero out the rect on error just to be safe + r->left = 0; + r->top = 0; + r->right = 0; + r->bottom = 0; + } +} diff --git a/src/libui_sdl/libui/windows/winutil.cpp b/src/libui_sdl/libui/windows/winutil.cpp new file mode 100644 index 0000000..507c5a3 --- /dev/null +++ b/src/libui_sdl/libui/windows/winutil.cpp @@ -0,0 +1,154 @@ +// 6 april 2015 +#include "uipriv_windows.hpp" + +// this is a helper function that takes the logic of determining window classes and puts it all in one place +// there are a number of places where we need to know what window class an arbitrary handle has +// theoretically we could use the class atom to avoid a _wcsicmp() +// however, raymond chen advises against this - http://blogs.msdn.com/b/oldnewthing/archive/2004/10/11/240744.aspx (and we're not in control of the Tab class, before you say anything) +// usage: windowClassOf(hwnd, L"class 1", L"class 2", ..., NULL) +int windowClassOf(HWND hwnd, ...) +{ +// MSDN says 256 is the maximum length of a class name; add a few characters just to be safe (because it doesn't say whether this includes the terminating null character) +#define maxClassName 260 + WCHAR classname[maxClassName + 1]; + va_list ap; + WCHAR *curname; + int i; + + if (GetClassNameW(hwnd, classname, maxClassName) == 0) { + logLastError(L"error getting name of window class"); + // assume no match on error, just to be safe + return -1; + } + va_start(ap, hwnd); + i = 0; + for (;;) { + curname = va_arg(ap, WCHAR *); + if (curname == NULL) + break; + if (_wcsicmp(classname, curname) == 0) { + va_end(ap); + return i; + } + i++; + } + // no match + va_end(ap); + return -1; +} + +// wrapper around MapWindowRect() that handles the complex error handling +void mapWindowRect(HWND from, HWND to, RECT *r) +{ + RECT prevr; + DWORD le; + + prevr = *r; + SetLastError(0); + if (MapWindowRect(from, to, r) == 0) { + le = GetLastError(); + SetLastError(le); // just to be safe + if (le != 0) { + logLastError(L"error calling MapWindowRect()"); + // restore original rect on error, just in case + *r = prevr; + } + } +} + +DWORD getStyle(HWND hwnd) +{ + return (DWORD) GetWindowLongPtrW(hwnd, GWL_STYLE); +} + +void setStyle(HWND hwnd, DWORD style) +{ + SetWindowLongPtrW(hwnd, GWL_STYLE, (LONG_PTR) style); +} + +DWORD getExStyle(HWND hwnd) +{ + return (DWORD) GetWindowLongPtrW(hwnd, GWL_EXSTYLE); +} + +void setExStyle(HWND hwnd, DWORD exstyle) +{ + SetWindowLongPtrW(hwnd, GWL_EXSTYLE, (LONG_PTR) exstyle); +} + +// see http://blogs.msdn.com/b/oldnewthing/archive/2003/09/11/54885.aspx and http://blogs.msdn.com/b/oldnewthing/archive/2003/09/13/54917.aspx +void clientSizeToWindowSize(HWND hwnd, int *width, int *height, BOOL hasMenubar) +{ + RECT window; + + window.left = 0; + window.top = 0; + window.right = *width; + window.bottom = *height; + if (AdjustWindowRectEx(&window, getStyle(hwnd), hasMenubar, getExStyle(hwnd)) == 0) { + logLastError(L"error getting adjusted window rect"); + // on error, don't give up; the window will be smaller but whatever + window.left = 0; + window.top = 0; + window.right = *width; + window.bottom = *height; + } + if (hasMenubar) { + RECT temp; + + temp = window; + temp.bottom = 0x7FFF; // infinite height + SendMessageW(hwnd, WM_NCCALCSIZE, (WPARAM) FALSE, (LPARAM) (&temp)); + window.bottom += temp.top; + } + *width = window.right - window.left; + *height = window.bottom - window.top; +} + +HWND parentOf(HWND child) +{ + return GetAncestor(child, GA_PARENT); +} + +HWND parentToplevel(HWND child) +{ + return GetAncestor(child, GA_ROOT); +} + +void setWindowInsertAfter(HWND hwnd, HWND insertAfter) +{ + if (SetWindowPos(hwnd, insertAfter, 0, 0, 0, 0, SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOOWNERZORDER | SWP_NOSIZE) == 0) + logLastError(L"error reordering window"); +} + +HWND getDlgItem(HWND hwnd, int id) +{ + HWND out; + + out = GetDlgItem(hwnd, id); + if (out == NULL) + logLastError(L"error getting dialog item handle"); + return out; +} + +void invalidateRect(HWND hwnd, RECT *r, BOOL erase) +{ + if (InvalidateRect(hwnd, r, erase) == 0) + logLastError(L"error invalidating window rect"); +} + +// that damn ABI bug is never going to escape me is it +D2D1_SIZE_F realGetSize(ID2D1RenderTarget *rt) +{ +#ifdef _MSC_VER + return rt->GetSize(); +#else + D2D1_SIZE_F size; + typedef D2D1_SIZE_F *(__stdcall ID2D1RenderTarget::* GetSizeF)(D2D1_SIZE_F *); + GetSizeF gs; + + gs = (GetSizeF) (&(rt->GetSize)); + (rt->*gs)(&size); + return size; +#endif +} |