From be610ecc52f3153d88e0c9e312dbe0dd3778b21e Mon Sep 17 00:00:00 2001 From: Magnus Norddahl Date: Sun, 7 Jan 2024 11:41:00 +0100 Subject: [PATCH] Sync latest changes from vkdoom --- include/zwidget/core/pathfill.h | 96 ++ include/zwidget/core/rect.h | 9 + include/zwidget/core/widget.h | 2 + include/zwidget/widgets/listview/listview.h | 9 + include/zwidget/widgets/scrollbar/scrollbar.h | 56 +- include/zwidget/window/window.h | 4 + src/core/canvas.cpp | 109 +++ src/core/pathfill.cpp | 613 +++++++++++++ src/core/truetypefont.cpp | 832 ++++++++++++++++++ src/core/truetypefont.h | 453 ++++++++++ src/core/widget.cpp | 28 +- src/widgets/lineedit/lineedit.cpp | 14 +- src/widgets/listview/listview.cpp | 83 +- src/widgets/scrollbar/scrollbar.cpp | 122 +-- src/window/sdl2/sdl2displaywindow.cpp | 676 ++++++++++++++ src/window/sdl2/sdl2displaywindow.h | 91 ++ src/window/win32/win32window.cpp | 69 +- src/window/win32/win32window.h | 7 + src/window/window.cpp | 41 +- 19 files changed, 3204 insertions(+), 110 deletions(-) create mode 100644 include/zwidget/core/pathfill.h create mode 100644 src/core/pathfill.cpp create mode 100644 src/core/truetypefont.cpp create mode 100644 src/core/truetypefont.h create mode 100644 src/window/sdl2/sdl2displaywindow.cpp create mode 100644 src/window/sdl2/sdl2displaywindow.h diff --git a/include/zwidget/core/pathfill.h b/include/zwidget/core/pathfill.h new file mode 100644 index 0000000..a6d7498 --- /dev/null +++ b/include/zwidget/core/pathfill.h @@ -0,0 +1,96 @@ +#pragma once + +#include +#include +#include +#include +#include "core/rect.h" + +/* +// 3x3 transform matrix +class PathFillTransform +{ +public: + PathFillTransform() { for (int i = 0; i < 9; i++) matrix[i] = 0.0f; matrix[0] = matrix[4] = matrix[8] = 1.0; } + PathFillTransform(const float* mat3x3) { for (int i = 0; i < 9; i++) matrix[i] = (double)mat3x3[i]; } + PathFillTransform(const double* mat3x3) { for (int i = 0; i < 9; i++) matrix[i] = mat3x3[i]; } + + double matrix[9]; +}; +*/ + +enum class PathFillMode +{ + alternate, + winding +}; + +enum class PathFillCommand +{ + line, + quadradic, + cubic +}; + +class PathFillSubpath +{ +public: + PathFillSubpath() : points(1) { } + + std::vector points; + std::vector commands; + bool closed = false; +}; + +class PathFillDesc +{ +public: + PathFillDesc() : subpaths(1) { } + + PathFillMode fill_mode = PathFillMode::alternate; + std::vector subpaths; + + void MoveTo(const Point& point) + { + if (!subpaths.back().commands.empty()) + { + subpaths.push_back(PathFillSubpath()); + } + subpaths.back().points.front() = point; + } + + void LineTo(const Point& point) + { + auto& subpath = subpaths.back(); + subpath.points.push_back(point); + subpath.commands.push_back(PathFillCommand::line); + } + + void BezierTo(const Point& control, const Point& point) + { + auto& subpath = subpaths.back(); + subpath.points.push_back(control); + subpath.points.push_back(point); + subpath.commands.push_back(PathFillCommand::quadradic); + } + + void BezierTo(const Point& control1, const Point& control2, const Point& point) + { + auto& subpath = subpaths.back(); + subpath.points.push_back(control1); + subpath.points.push_back(control2); + subpath.points.push_back(point); + subpath.commands.push_back(PathFillCommand::cubic); + } + + void Close() + { + if (!subpaths.back().commands.empty()) + { + subpaths.back().closed = true; + subpaths.push_back(PathFillSubpath()); + } + } + + void Rasterize(uint8_t* dest, int width, int height, bool blend = false); +}; diff --git a/include/zwidget/core/rect.h b/include/zwidget/core/rect.h index f5ab17c..ad2a4e0 100644 --- a/include/zwidget/core/rect.h +++ b/include/zwidget/core/rect.h @@ -46,6 +46,15 @@ class Rect static Rect xywh(double x, double y, double width, double height) { return Rect(x, y, width, height); } static Rect ltrb(double left, double top, double right, double bottom) { return Rect(left, top, right - left, bottom - top); } + static Rect shrink(Rect box, double left, double top, double right, double bottom) + { + box.x += left; + box.y += top; + box.width = std::max(box.width - left - right, 0.0); + box.height = std::max(box.height - bottom - top, 0.0); + return box; + } + bool contains(const Point& p) const { return (p.x >= x && p.x < x + width) && (p.y >= y && p.y < y + height); } double x = 0; diff --git a/include/zwidget/core/widget.h b/include/zwidget/core/widget.h index a889938..69862fd 100644 --- a/include/zwidget/core/widget.h +++ b/include/zwidget/core/widget.h @@ -177,6 +177,8 @@ class Widget : DisplayWindowHost Widget* CaptureWidget = nullptr; Widget* HoverWidget = nullptr; + StandardCursor CurrentCursor = StandardCursor::arrow; + Widget(const Widget&) = delete; Widget& operator=(const Widget&) = delete; diff --git a/include/zwidget/widgets/listview/listview.h b/include/zwidget/widgets/listview/listview.h index 0d8f632..c7caca8 100644 --- a/include/zwidget/widgets/listview/listview.h +++ b/include/zwidget/widgets/listview/listview.h @@ -5,6 +5,8 @@ #include #include +class Scrollbar; + class ListView : public Widget { public: @@ -12,6 +14,8 @@ class ListView : public Widget void AddItem(const std::string& text); int GetSelectedItem() const { return selectedItem; } + void SetSelectedItem(int index); + void ScrollToItem(int index); void Activate(); @@ -22,7 +26,12 @@ class ListView : public Widget void OnPaintFrame(Canvas* canvas) override; void OnMouseDown(const Point& pos, int key) override; void OnMouseDoubleclick(const Point& pos, int key) override; + void OnMouseWheel(const Point& pos, EInputKey key) override; void OnKeyDown(EInputKey key) override; + void OnGeometryChanged() override; + void OnScrollbarScroll(); + + Scrollbar* scrollbar = nullptr; std::vector items; int selectedItem = 0; diff --git a/include/zwidget/widgets/scrollbar/scrollbar.h b/include/zwidget/widgets/scrollbar/scrollbar.h index f582618..38afda3 100644 --- a/include/zwidget/widgets/scrollbar/scrollbar.h +++ b/include/zwidget/widgets/scrollbar/scrollbar.h @@ -13,24 +13,27 @@ class Scrollbar : public Widget bool IsVertical() const; bool IsHorizontal() const; - int GetMin() const; - int GetMax() const; - int GetLineStep() const; - int GetPageStep() const; - int GetPosition() const; + double GetMin() const; + double GetMax() const; + double GetLineStep() const; + double GetPageStep() const; + double GetPosition() const; void SetVertical(); void SetHorizontal(); - void SetMin(int scroll_min); - void SetMax(int scroll_max); - void SetLineStep(int step); - void SetPageStep(int step); + void SetMin(double scroll_min); + void SetMax(double scroll_max); + void SetLineStep(double step); + void SetPageStep(double step); - void SetRanges(int scroll_min, int scroll_max, int line_step, int page_step); - void SetRanges(int view_size, int total_size); + void SetRanges(double scroll_min, double scroll_max, double line_step, double page_step); + void SetRanges(double view_size, double total_size); - void SetPosition(int pos); + void SetPosition(double pos); + + double GetPreferredWidth() const { return 16.0; } + double GetPreferredHeight() const { return 16.0; } std::function FuncScroll; std::function FuncScrollMin; @@ -54,18 +57,20 @@ class Scrollbar : public Widget private: bool UpdatePartPositions(); - int CalculateThumbSize(int track_size); - int CalculateThumbPosition(int thumb_size, int track_size); - Rect CreateRect(int start, int end); + double CalculateThumbSize(double track_size); + double CalculateThumbPosition(double thumb_size, double track_size); + Rect CreateRect(double start, double end); void InvokeScrollEvent(std::function* event_ptr); void OnTimerExpired(); - bool vertical = false; - int scroll_min = 0; - int scroll_max = 1; - int line_step = 1; - int page_step = 10; - int position = 0; + bool vertical = true; + double scroll_min = 0.0; + double scroll_max = 1.0; + double line_step = 1.0; + double page_step = 10.0; + double position = 0.0; + + bool showbuttons = false; enum MouseDownMode { @@ -77,12 +82,12 @@ class Scrollbar : public Widget mouse_down_thumb_drag } mouse_down_mode = mouse_down_none; - int thumb_start_position = 0; + double thumb_start_position = 0.0; Point mouse_drag_start_pos; - int thumb_start_pixel_position = 0; + double thumb_start_pixel_position = 0.0; Timer* mouse_down_timer = nullptr; - int last_step_size = 0; + double last_step_size = 0.0; Rect rect_button_decrement; Rect rect_track_decrement; @@ -91,7 +96,4 @@ class Scrollbar : public Widget Rect rect_button_increment; std::function* FuncScrollOnMouseDown = nullptr; - - static const int decr_height = 16; - static const int incr_height = 16; }; diff --git a/include/zwidget/window/window.h b/include/zwidget/window/window.h index 5798e78..0539f77 100644 --- a/include/zwidget/window/window.h +++ b/include/zwidget/window/window.h @@ -162,9 +162,13 @@ class DisplayWindow virtual void ShowCursor(bool enable) = 0; virtual void LockCursor() = 0; virtual void UnlockCursor() = 0; + virtual void CaptureMouse() = 0; + virtual void ReleaseMouseCapture() = 0; virtual void Update() = 0; virtual bool GetKeyState(EInputKey key) = 0; + virtual void SetCursor(StandardCursor cursor) = 0; + virtual Rect GetWindowFrame() const = 0; virtual Size GetClientSize() const = 0; virtual int GetPixelWidth() const = 0; diff --git a/src/core/canvas.cpp b/src/core/canvas.cpp index c92101b..fe4e415 100644 --- a/src/core/canvas.cpp +++ b/src/core/canvas.cpp @@ -5,6 +5,8 @@ #include "core/utf8reader.h" #include "core/resourcedata.h" #include "core/image.h" +#include "core/truetypefont.h" +#include "core/pathfill.h" #include "window/window.h" #include "schrift/schrift.h" #include @@ -12,6 +14,8 @@ #include #include +// #define USE_INTERNAL_TTF + class CanvasTexture { public: @@ -20,6 +24,109 @@ class CanvasTexture std::vector Data; }; +#if defined(USE_INTERNAL_TTF) + +class CanvasGlyph +{ +public: + struct + { + double leftSideBearing = 0.0; + double yOffset = 0.0; + double advanceWidth = 0.0; + } metrics; + + double u = 0.0; + double v = 0.0; + double uvwidth = 0.0f; + double uvheight = 0.0f; + std::shared_ptr texture; +}; + +class CanvasFont +{ +public: + CanvasFont(const std::string& fontname, double height) : fontname(fontname), height(height) + { + ttf = std::make_unique(LoadWidgetFontData(fontname)); + textmetrics = ttf->GetTextMetrics(height); + } + + ~CanvasFont() + { + } + + CanvasGlyph* getGlyph(uint32_t utfchar) + { + uint32_t glyphIndex = ttf->GetGlyphIndex(utfchar); + + auto& glyph = glyphs[glyphIndex]; + if (glyph) + return glyph.get(); + + glyph = std::make_unique(); + + TrueTypeGlyph ttfglyph = ttf->LoadGlyph(glyphIndex, height); + + // Create final subpixel version + int w = ttfglyph.width; + int h = ttfglyph.height; + int destwidth = (w + 2) / 3; + auto texture = std::make_shared(); + texture->Width = destwidth; + texture->Height = h; + texture->Data.resize(destwidth * h); + + uint8_t* grayscale = ttfglyph.grayscale.get(); + uint32_t* dest = (uint32_t*)texture->Data.data(); + for (int y = 0; y < h; y++) + { + uint8_t* sline = grayscale + y * w; + uint32_t* dline = dest + y * destwidth; + for (int x = 0; x < w; x += 3) + { + uint32_t values[5] = + { + x > 0 ? sline[x - 1] : 0U, + sline[x], + x + 1 < w ? sline[x + 1] : 0U, + x + 2 < w ? sline[x + 2] : 0U, + x + 3 < w ? sline[x + 3] : 0U + }; + + uint32_t red = (values[0] + values[1] + values[1] + values[2] + 2) >> 2; + uint32_t green = (values[1] + values[2] + values[2] + values[3] + 2) >> 2; + uint32_t blue = (values[2] + values[3] + values[3] + values[4] + 2) >> 2; + uint32_t alpha = (red | green | blue) ? 255 : 0; + + *(dline++) = (alpha << 24) | (red << 16) | (green << 8) | blue; + } + } + + glyph->u = 0.0; + glyph->v = 0.0; + glyph->uvwidth = destwidth; + glyph->uvheight = h; + glyph->texture = std::move(texture); + + glyph->metrics.advanceWidth = (ttfglyph.advanceWidth + 2) / 3; + glyph->metrics.leftSideBearing = (ttfglyph.leftSideBearing + 2) / 3; + glyph->metrics.yOffset = ttfglyph.yOffset; + + return glyph.get(); + } + + std::unique_ptr ttf; + + std::string fontname; + double height = 0.0; + + TrueTypeTextMetrics textmetrics; + std::unordered_map> glyphs; +}; + +#else + class CanvasGlyph { public: @@ -152,6 +259,8 @@ class CanvasFont std::vector data; }; +#endif + class BitmapCanvas : public Canvas { public: diff --git a/src/core/pathfill.cpp b/src/core/pathfill.cpp new file mode 100644 index 0000000..bac3dfb --- /dev/null +++ b/src/core/pathfill.cpp @@ -0,0 +1,613 @@ + +#include "core/pathfill.h" +#include +#include + +static const int AntialiasLevel = 8; +static const int MaskBlockSize = 16; +static const int MaskBufferSize = MaskBlockSize * MaskBlockSize; +static const int ScanlineBlockSize = MaskBlockSize * AntialiasLevel; + +class PathScanlineEdge +{ +public: + PathScanlineEdge() = default; + PathScanlineEdge(double x, bool up_direction) : x(x), up_direction(up_direction) { } + + double x = 0.0; + bool up_direction = false; +}; + +class PathScanline +{ +public: + std::vector edges; + std::vector pixels; + + void insert_sorted(PathScanlineEdge edge) + { + edges.push_back(edge); + + for (size_t pos = edges.size() - 1; pos > 0 && edges[pos - 1].x >= edge.x; pos--) + { + PathScanlineEdge temp = edges[pos - 1]; + edges[pos - 1] = edges[pos]; + edges[pos] = temp; + } + } +}; + +class PathRasterRange +{ +public: + void Begin(const PathScanline* scanline, PathFillMode mode); + void Next(); + + bool found = false; + int x0; + int x1; + +private: + const PathScanline* scanline = nullptr; + PathFillMode mode; + size_t i = 0; + int nonzero_rule = 0; +}; + +enum class PathFillBlockResult +{ + Empty, + Partial, + Full +}; + +class PathMaskBuffer +{ +public: + void BeginRow(PathScanline* scanlines, PathFillMode mode); + PathFillBlockResult FillBlock(int xpos); + + unsigned char MaskBufferData[MaskBufferSize]; + +private: + bool IsFullBlock(int xpos) const; + + PathRasterRange Range[ScanlineBlockSize]; +}; + +class PathFillRasterizer +{ +public: + void Rasterize(const PathFillDesc& path, uint8_t* dest, int width, int height); + +private: + void Clear(); + + void Begin(double x, double y); + void QuadraticBezier(double cp1_x, double cp1_y, double cp2_x, double cp2_y); + void CubicBezier(double cp1_x, double cp1_y, double cp2_x, double cp2_y, double cp3_x, double cp3_y); + void Line(double x, double y); + void End(bool close); + + void SubdivideBezier(int level, double cp0_x, double cp0_y, double cp1_x, double cp1_y, double cp2_x, double cp2_y, double cp3_x, double cp3_y, double t0, double t1); + static Point PointOnBezier(double cp0_x, double cp0_y, double cp1_x, double cp1_y, double cp2_x, double cp2_y, double cp3_x, double cp3_y, double t); + + void Fill(PathFillMode mode, uint8_t* dest, int dest_width, int dest_height); + + struct Extent + { + Extent() : left(INT_MAX), right(0) {} + int left; + int right; + }; + + Extent FindExtent(const PathScanline* scanline, int max_width); + + double start_x = 0.0; + double start_y = 0.0; + double last_x = 0.0; + double last_y = 0.0; + + int first_scanline = 0; + int last_scanline = 0; + + int width = 0; + int height = 0; + std::vector scanlines; + + PathMaskBuffer mask_blocks; +}; + +///////////////////////////////////////////////////////////////////////////// + +void PathFillDesc::Rasterize(uint8_t* dest, int width, int height, bool blend) +{ + if (!blend) + { + memset(dest, 0, width * height); + } + PathFillRasterizer rasterizer; + rasterizer.Rasterize(*this, dest, width, height); +} + +///////////////////////////////////////////////////////////////////////////// + +void PathFillRasterizer::Rasterize(const PathFillDesc& path, uint8_t* dest, int dest_width, int dest_height) +{ + Clear(); + + // For simplicity of the code, ensure the mask is always a multiple of MaskBlockSize + int block_width = ScanlineBlockSize * ((dest_width + MaskBlockSize - 1) / MaskBlockSize); + int block_height = ScanlineBlockSize * ((dest_height + MaskBlockSize - 1) / MaskBlockSize); + + if (width != block_width || height != block_height) + { + width = block_width; + height = block_height; + + scanlines.resize(block_height); + first_scanline = scanlines.size(); + last_scanline = 0; + } + + for (const auto& subpath : path.subpaths) + { + Point start_point = subpath.points[0]; + Begin(start_point.x, start_point.y); + + size_t i = 1; + for (PathFillCommand command : subpath.commands) + { + if (command == PathFillCommand::line) + { + const Point& next_point = subpath.points[i]; + i++; + + Line(next_point.x, next_point.y); + } + else if (command == PathFillCommand::quadradic) + { + const Point& control = subpath.points[i]; + const Point& next_point = subpath.points[i + 1]; + i += 2; + + QuadraticBezier(control.x, control.y, next_point.x, next_point.y); + } + else if (command == PathFillCommand::cubic) + { + const Point& control1 = subpath.points[i]; + const Point& control2 = subpath.points[i + 1]; + const Point& next_point = subpath.points[i + 2]; + i += 3; + + CubicBezier(control1.x, control1.y, control2.x, control2.y, next_point.x, next_point.y); + } + } + + End(subpath.closed); + } + + Fill(path.fill_mode, dest, dest_width, dest_height); +} + +void PathFillRasterizer::Fill(PathFillMode mode, uint8_t* dest, int dest_width, int dest_height) +{ + if (scanlines.empty()) return; + + int start_y = first_scanline / ScanlineBlockSize * ScanlineBlockSize; + int end_y = (last_scanline + ScanlineBlockSize - 1) / ScanlineBlockSize * ScanlineBlockSize; + + for (int ypos = start_y; ypos < end_y; ypos += ScanlineBlockSize) + { + mask_blocks.BeginRow(&scanlines[ypos], mode); + + Extent extent = FindExtent(&scanlines[ypos], width); + for (int xpos = extent.left; xpos < extent.right; xpos += ScanlineBlockSize) + { + PathFillBlockResult result = mask_blocks.FillBlock(xpos); + + int dest_x = xpos / AntialiasLevel; + int dest_y = ypos / AntialiasLevel; + int count_x = std::min(dest_x + MaskBlockSize, dest_width) - dest_x; + int count_y = std::min(dest_y + MaskBlockSize, dest_height) - dest_y; + + if (result == PathFillBlockResult::Full) + { + for (int i = 0; i < count_y; i++) + { + uint8_t* dline = dest + dest_x + (dest_y + i) * dest_width; + memset(dline, 255, count_x); + } + } + else if (result == PathFillBlockResult::Partial) + { + for (int i = 0; i < count_y; i++) + { + const uint8_t* sline = mask_blocks.MaskBufferData + i * MaskBlockSize; + uint8_t* dline = dest + dest_x + (dest_y + i) * dest_width; + for (int j = 0; j < count_x; j++) + { + dline[j] = std::min((int)dline[j] + (int)sline[j], 255); + } + } + } + } + } +} + +PathFillRasterizer::Extent PathFillRasterizer::FindExtent(const PathScanline* scanline, int max_width) +{ + // Find scanline extents + Extent extent; + for (unsigned int cnt = 0; cnt < ScanlineBlockSize; cnt++, scanline++) + { + if (scanline->edges.empty()) + continue; + + extent.left = std::min(extent.left, (int)scanline->edges.front().x); + extent.right = std::max(extent.right, (int)scanline->edges.back().x); + } + extent.left = std::max(extent.left, 0); + extent.right = std::min(extent.right, max_width); + + return extent; +} + +void PathFillRasterizer::Clear() +{ + for (size_t y = first_scanline; y < last_scanline; y++) + { + auto& scanline = scanlines[y]; + if (!scanline.edges.empty()) + { + scanline.edges.clear(); + } + } + + first_scanline = scanlines.size(); + last_scanline = 0; +} + +void PathFillRasterizer::Begin(double x, double y) +{ + start_x = last_x = x; + start_y = last_y = y; +} + +void PathFillRasterizer::End(bool close) +{ + if (close) + { + Line(start_x, start_y); + } +} + +void PathFillRasterizer::QuadraticBezier(double qcp1_x, double qcp1_y, double qcp2_x, double qcp2_y) +{ + double qcp0_x = last_x; + double qcp0_y = last_y; + + // Convert to cubic: + double cp1_x = qcp0_x + 2.0 * (qcp1_x - qcp0_x) / 3.0; + double cp1_y = qcp0_y + 2.0 * (qcp1_y - qcp0_y) / 3.0; + double cp2_x = qcp1_x + (qcp2_x - qcp1_x) / 3.0; + double cp2_y = qcp1_y + (qcp2_y - qcp1_y) / 3.0; + double cp3_x = qcp2_x; + double cp3_y = qcp2_y; + CubicBezier(cp1_x, cp1_y, cp2_x, cp2_y, cp3_x, cp3_y); +} + +void PathFillRasterizer::CubicBezier(double cp1_x, double cp1_y, double cp2_x, double cp2_y, double cp3_x, double cp3_y) +{ + double cp0_x = last_x; + double cp0_y = last_y; + + double estimated_length = + std::sqrt((cp1_x - cp0_x) * (cp1_x - cp0_x) + (cp1_y - cp0_y) * (cp1_y - cp0_y)) + + std::sqrt((cp1_x - cp0_x) * (cp1_x - cp0_x) + (cp1_y - cp0_y) * (cp1_y - cp0_y)) + + std::sqrt((cp1_x - cp0_x) * (cp1_x - cp0_x) + (cp1_y - cp0_y) * (cp1_y - cp0_y)); + + double min_segs = 10.0; + double segs = estimated_length / 5.0; + int steps = (int)std::ceil(std::sqrt(segs * segs * 0.3f + min_segs)); + for (int i = 0; i < steps; i++) + { + //Point sp = PointOnBezier(cp0_x, cp0_y, cp1_x, cp1_y, cp2_x, cp2_y, cp3_x, cp3_y, i / (double)steps); + Point ep = PointOnBezier(cp0_x, cp0_y, cp1_x, cp1_y, cp2_x, cp2_y, cp3_x, cp3_y, (i + 1) / (double)steps); + Line(ep.x, ep.y); + } + + // http://ciechanowski.me/blog/2014/02/18/drawing-bezier-curves/ + // http://antigrain.com/research/adaptive_bezier/ (best method, unfortunately GPL example code) +} + +void PathFillRasterizer::SubdivideBezier(int level, double cp0_x, double cp0_y, double cp1_x, double cp1_y, double cp2_x, double cp2_y, double cp3_x, double cp3_y, double t0, double t1) +{ + const double split_angle_cos = 0.99f; + + double tc = (t0 + t1) * 0.5f; + + Point sp = PointOnBezier(cp0_x, cp0_y, cp1_x, cp1_y, cp2_x, cp2_y, cp3_x, cp3_y, t0); + Point cp = PointOnBezier(cp0_x, cp0_y, cp1_x, cp1_y, cp2_x, cp2_y, cp3_x, cp3_y, tc); + Point ep = PointOnBezier(cp0_x, cp0_y, cp1_x, cp1_y, cp2_x, cp2_y, cp3_x, cp3_y, t1); + + Point sp2cp(cp.x - sp.x, cp.y - sp.y); + Point cp2ep(ep.x - cp.x, ep.y - cp.y); + + // Normalize + double len_sp2cp = std::sqrt(sp2cp.x * sp2cp.x + sp2cp.y * sp2cp.y); + double len_cp2ep = std::sqrt(cp2ep.x * cp2ep.x + cp2ep.y * cp2ep.y); + if (len_sp2cp > 0.0) { sp2cp.x /= len_sp2cp; sp2cp.y /= len_sp2cp; } + if (len_cp2ep > 0.0) { cp2ep.x /= len_cp2ep; cp2ep.y /= len_cp2ep; } + + double dot = sp2cp.x * cp2ep.x + sp2cp.y * cp2ep.y; + if (dot < split_angle_cos && level < 15) + { + SubdivideBezier(level + 1, cp0_x, cp0_y, cp1_x, cp1_y, cp2_x, cp2_y, cp3_x, cp3_y, t0, tc); + SubdivideBezier(level + 1, cp0_x, cp0_y, cp1_x, cp1_y, cp2_x, cp2_y, cp3_x, cp3_y, tc, t1); + } + else + { + Line(ep.x, ep.y); + } +} + +Point PathFillRasterizer::PointOnBezier(double cp0_x, double cp0_y, double cp1_x, double cp1_y, double cp2_x, double cp2_y, double cp3_x, double cp3_y, double t) +{ + const int num_cp = 4; + + double cp_x[4] = { cp0_x, cp1_x, cp2_x, cp3_x }; + double cp_y[4] = { cp0_y, cp1_y, cp2_y, cp3_y }; + + // Perform deCasteljau iterations: + // (linear interpolate between the control points) + double a = 1.0 - t; + double b = t; + for (int j = num_cp - 1; j > 0; j--) + { + for (int i = 0; i < j; i++) + { + cp_x[i] = a * cp_x[i] + b * cp_x[i + 1]; + cp_y[i] = a * cp_y[i] + b * cp_y[i + 1]; + } + } + + return Point(cp_x[0], cp_y[0]); +} + +void PathFillRasterizer::Line(double x1, double y1) +{ + double x0 = last_x; + double y0 = last_y; + + last_x = x1; + last_y = y1; + + x0 *= static_cast(AntialiasLevel); + x1 *= static_cast(AntialiasLevel); + y0 *= static_cast(AntialiasLevel); + y1 *= static_cast(AntialiasLevel); + + bool up_direction = y1 < y0; + double dy = y1 - y0; + + constexpr const double epsilon = std::numeric_limits::epsilon(); + if (dy < -epsilon || dy > epsilon) + { + int start_y = static_cast(std::floor(std::min(y0, y1) + 0.5f)); + int end_y = static_cast(std::floor(std::max(y0, y1) - 0.5f)) + 1; + + start_y = std::max(start_y, 0); + end_y = std::min(end_y, height); + + double rcp_dy = 1.0 / dy; + + first_scanline = std::min(first_scanline, start_y); + last_scanline = std::max(last_scanline, end_y); + + for (int y = start_y; y < end_y; y++) + { + double ypos = y + 0.5f; + double x = x0 + (x1 - x0) * (ypos - y0) * rcp_dy; + scanlines[y].insert_sorted(PathScanlineEdge(x, up_direction)); + } + } +} + +///////////////////////////////////////////////////////////////////////////// + +void PathRasterRange::Begin(const PathScanline* new_scanline, PathFillMode new_mode) +{ + scanline = new_scanline; + mode = new_mode; + found = false; + i = 0; + nonzero_rule = 0; + Next(); +} + +void PathRasterRange::Next() +{ + if (i + 1 >= scanline->edges.size()) + { + found = false; + return; + } + + if (mode == PathFillMode::alternate) + { + x0 = static_cast(scanline->edges[i].x + 0.5f); + x1 = static_cast(scanline->edges[i + 1].x - 0.5f) + 1; + i += 2; + found = true; + } + else + { + x0 = static_cast(scanline->edges[i].x + 0.5f); + nonzero_rule += scanline->edges[i].up_direction ? 1 : -1; + i++; + + while (i < scanline->edges.size()) + { + nonzero_rule += scanline->edges[i].up_direction ? 1 : -1; + x1 = static_cast(scanline->edges[i].x - 0.5f) + 1; + i++; + + if (nonzero_rule == 0) + { + found = true; + return; + } + } + found = false; + } +} + +///////////////////////////////////////////////////////////////////////// + +void PathMaskBuffer::BeginRow(PathScanline* scanlines, PathFillMode mode) +{ + for (unsigned int cnt = 0; cnt < ScanlineBlockSize; cnt++) + { + Range[cnt].Begin(&scanlines[cnt], mode); + } +} + +#if 0 +PathFillBlockResult PathMaskBuffer::FillBlock(int xpos) +{ + if (IsFullBlock(xpos)) + { + return PathFillBlockResult::Full; + } + + const int block_size = MaskBlockSize / 16 * MaskBlockSize; + __m128i block[block_size]; + + for (auto& elem : block) + elem = _mm_setzero_si128(); + + for (unsigned int cnt = 0; cnt < ScanlineBlockSize; cnt++) + { + __m128i* line = &block[MaskBlockSize / 16 * (cnt / AntialiasLevel)]; + + while (range[cnt].found) + { + int x0 = range[cnt].x0; + if (x0 >= xpos + ScanlineBlockSize) + break; + int x1 = range[cnt].x1; + + x0 = max(x0, xpos); + x1 = min(x1, xpos + ScanlineBlockSize); + + if (x0 >= x1) // Done segment + { + range[cnt].next(); + } + else + { + for (int sse_block = 0; sse_block < MaskBlockSize / 16; sse_block++) + { + for (int alias_cnt = 0; alias_cnt < (AntialiasLevel); alias_cnt++) + { + __m128i start = _mm_set1_epi8((x0 + alias_cnt - xpos) / AntialiasLevel - 16 * sse_block); + __m128i end = _mm_set1_epi8((x1 + alias_cnt - xpos) / AntialiasLevel - 16 * sse_block); + __m128i x = _mm_set_epi8(15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0); + + __m128i left = _mm_cmplt_epi8(x, start); + __m128i right = _mm_cmplt_epi8(x, end); + __m128i mask = _mm_andnot_si128(left, right); + __m128i add_value = _mm_and_si128(mask, _mm_set1_epi8(256 / (AntialiasLevel * AntialiasLevel))); + + line[sse_block] = _mm_adds_epu8(line[sse_block], add_value); + } + } + + range[cnt].x0 = x1; // For next time + } + } + } + + __m128i empty_status = _mm_setzero_si128(); + for (auto& elem : block) + empty_status = _mm_or_si128(empty_status, elem); + + bool empty_block = _mm_movemask_epi8(_mm_cmpeq_epi32(empty_status, _mm_setzero_si128())) == 0xffff; + if (empty_block) return PathFillBlockResult::Empty; + + for (unsigned int cnt = 0; cnt < MaskBlockSize; cnt++) + { + __m128i* input = &block[MaskBlockSize / 16 * cnt]; + __m128i* output = (__m128i*)(MaskBufferData + cnt * mask_texture_size); + + for (int sse_block = 0; sse_block < MaskBlockSize / 16; sse_block++) + _mm_storeu_si128(&output[sse_block], input[sse_block]); + } + + return PathFillBlockResult::Partial; +} + +#else + +PathFillBlockResult PathMaskBuffer::FillBlock(int xpos) +{ + if (IsFullBlock(xpos)) + { + return PathFillBlockResult::Full; + } + + memset(MaskBufferData, 0, MaskBufferSize); + + bool empty_block = true; + for (unsigned int cnt = 0; cnt < ScanlineBlockSize; cnt++) + { + unsigned char* line = MaskBufferData + MaskBlockSize * (cnt / AntialiasLevel); + while (Range[cnt].found) + { + int x0 = Range[cnt].x0; + if (x0 >= xpos + ScanlineBlockSize) + break; + int x1 = Range[cnt].x1; + + x0 = std::max(x0, xpos); + x1 = std::min(x1, xpos + ScanlineBlockSize); + + if (x0 >= x1) // Done segment + { + Range[cnt].Next(); + } + else + { + empty_block = false; + for (int x = x0 - xpos; x < x1 - xpos; x++) + { + int pixel = line[x / AntialiasLevel]; + pixel = std::min(pixel + (256 / (AntialiasLevel * AntialiasLevel)), 255); + line[x / AntialiasLevel] = pixel; + } + Range[cnt].x0 = x1; // For next time + } + } + } + + return empty_block ? PathFillBlockResult::Empty : PathFillBlockResult::Partial; +} + +#endif + +bool PathMaskBuffer::IsFullBlock(int xpos) const +{ + for (auto& elem : Range) + { + if (!elem.found) + { + return false; + } + if ((elem.x0 > xpos) || (elem.x1 < (xpos + ScanlineBlockSize - 1))) + { + return false; + } + } + return true; +} diff --git a/src/core/truetypefont.cpp b/src/core/truetypefont.cpp new file mode 100644 index 0000000..867898c --- /dev/null +++ b/src/core/truetypefont.cpp @@ -0,0 +1,832 @@ + +#include "core/truetypefont.h" +#include "core/pathfill.h" +#include +#include + +TrueTypeFont::TrueTypeFont(std::vector initdata) : data(std::move(initdata)) +{ + if (data.size() > 0x7fffffff) + throw std::runtime_error("TTF file is larger than 2 gigabytes!"); + + TrueTypeFileReader reader(data.data(), data.size()); + directory.Load(reader); + + if (!directory.ContainsTTFOutlines()) + throw std::runtime_error("Only truetype outline fonts are supported"); + + // Load required tables: + + reader = directory.GetReader(data.data(), data.size(), "head"); + head.Load(reader); + + reader = directory.GetReader(data.data(), data.size(), "hhea"); + hhea.Load(reader); + + reader = directory.GetReader(data.data(), data.size(), "maxp"); + maxp.Load(reader); + + reader = directory.GetReader(data.data(), data.size(), "hmtx"); + hmtx.Load(hhea, maxp, reader); + + reader = directory.GetReader(data.data(), data.size(), "name"); + name.Load(reader); + + reader = directory.GetReader(data.data(), data.size(), "OS/2"); + os2.Load(reader); + + reader = directory.GetReader(data.data(), data.size(), "cmap"); + cmap.Load(reader); + + LoadCharacterMapEncoding(reader); + + // Load TTF Outlines: + + reader = directory.GetReader(data.data(), data.size(), "loca"); + loca.Load(head, maxp, reader); + + glyf = directory.GetRecord("glyf"); + + LoadGlyph(GetGlyphIndex(32), 13.0); +} + +TrueTypeTextMetrics TrueTypeFont::GetTextMetrics(double height) const +{ + double scale = height / head.unitsPerEm; + + TrueTypeTextMetrics metrics; + metrics.ascender = os2.sTypoAscender * scale; + metrics.descender = os2.sTypoDescender * scale; + metrics.lineGap = os2.sTypoLineGap * scale; + return metrics; +} + +TrueTypeGlyph TrueTypeFont::LoadGlyph(uint32_t glyphIndex, double height) const +{ + if (glyphIndex >= loca.offsets.size()) + throw std::runtime_error("Glyph index out of bounds"); + + double scale = height / head.unitsPerEm; + double scaleX = 3.0f; + double scaleY = -1.0f; + + ttf_uint16 advanceWidth = 0; + ttf_int16 lsb = 0; + if (glyphIndex >= hhea.numberOfHMetrics) + { + advanceWidth = hmtx.hMetrics[hhea.numberOfHMetrics - 1].advanceWidth; + lsb = hmtx.leftSideBearings[glyphIndex - hhea.numberOfHMetrics]; + } + else + { + advanceWidth = hmtx.hMetrics[glyphIndex].advanceWidth; + lsb = hmtx.hMetrics[glyphIndex].lsb; + } + + // Glyph is missing if the offset is the same as the next glyph (0 bytes glyph length) + bool missing = glyphIndex + 1 < loca.offsets.size() ? loca.offsets[glyphIndex] == loca.offsets[glyphIndex + 1] : false; + if (missing) + { + TrueTypeGlyph glyph; + + // TBD: gridfit or not? + glyph.advanceWidth = (int)std::round(advanceWidth * scale * scaleX); + glyph.leftSideBearing = (int)std::round(lsb * scale * scaleX); + glyph.yOffset = 0; + + return glyph; + } + + TrueTypeFileReader reader = glyf.GetReader(data.data(), data.size()); + reader.Seek(loca.offsets[glyphIndex]); + + ttf_int16 numberOfContours = reader.ReadInt16(); + ttf_int16 xMin = reader.ReadInt16(); + ttf_int16 yMin = reader.ReadInt16(); + ttf_int16 xMax = reader.ReadInt16(); + ttf_int16 yMax = reader.ReadInt16(); + + if (numberOfContours > 0) // Simple glyph + { + std::vector endPtsOfContours; + endPtsOfContours.reserve(numberOfContours); + for (ttf_uint16 i = 0; i < numberOfContours; i++) + endPtsOfContours.push_back(reader.ReadUInt16()); + + ttf_uint16 instructionLength = reader.ReadUInt16(); + std::vector instructions; + instructions.resize(instructionLength); + reader.Read(instructions.data(), instructions.size()); + + int numPoints = (int)endPtsOfContours.back() + 1; + std::vector points(numPoints); + + std::vector flags; + + while (flags.size() < (size_t)numPoints) + { + ttf_uint8 flag = reader.ReadUInt8(); + if (flag & TTF_REPEAT_FLAG) + { + ttf_uint8 repeatcount = reader.ReadUInt8(); + for (ttf_uint8 i = 0; i < repeatcount; i++) + flags.push_back(flag); + } + flags.push_back(flag); + } + + for (int i = 0; i < numPoints; i++) + { + if (flags[i] & TTF_X_SHORT_VECTOR) + { + ttf_int16 x = reader.ReadUInt8(); + points[i].x = (flags[i] & TTF_X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR) ? x : -x; + } + else if (flags[i] & TTF_X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR) + { + points[i].x = 0; + } + else + { + points[i].x = reader.ReadInt16(); + } + } + + for (int i = 0; i < numPoints; i++) + { + if (flags[i] & TTF_Y_SHORT_VECTOR) + { + ttf_int16 y = reader.ReadUInt8(); + points[i].y = (flags[i] & TTF_Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR) ? y : -y; + } + else if (flags[i] & TTF_Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR) + { + points[i].y = 0; + } + else + { + points[i].y = reader.ReadInt16(); + } + } + + // Convert from relative coordinates to absolute + for (int i = 1; i < numPoints; i++) + { + points[i].x += points[i - 1].x; + points[i].y += points[i - 1].y; + } + + // Create glyph path: + PathFillDesc path; + path.fill_mode = PathFillMode::winding; + + int startPoint = 0; + for (ttf_uint16 i = 0; i < numberOfContours; i++) + { + int endPoint = endPtsOfContours[i]; + if (endPoint < startPoint) + throw std::runtime_error("Invalid glyph"); + + int pos = startPoint; + while (pos <= endPoint) + { + if (pos == startPoint) + { + path.MoveTo(Point(points[pos].x, points[pos].y) * scale); + pos++; + } + else if (flags[pos] & TTF_ON_CURVE_POINT) + { + if (flags[pos - 1] & TTF_ON_CURVE_POINT) + { + path.LineTo(Point(points[pos].x, points[pos].y) * scale); + } + else + { + path.BezierTo(Point(points[pos - 1].x, points[pos - 1].y) * scale, Point(points[pos].x, points[pos].y) * scale); + } + pos++; + } + else + { + Point lastcontrolpoint(points[pos].x, points[pos].y); + Point controlpoint(points[pos - 1].x, points[pos - 1].y); + Point midpoint = (lastcontrolpoint + controlpoint) / 2; + path.BezierTo(lastcontrolpoint * scale, midpoint * scale); + pos++; + } + } + path.Close(); + + startPoint = endPoint + 1; + } + + // Transform and find the final bounding box + Point bboxMin, bboxMax; + if (!path.subpaths.front().points.empty()) + { + bboxMin = path.subpaths.front().points.front(); + bboxMax = path.subpaths.front().points.front(); + bboxMin.x *= scaleX; + bboxMin.y *= scaleY; + bboxMax.x *= scaleX; + bboxMax.y *= scaleY; + for (auto& subpath : path.subpaths) + { + for (auto& point : subpath.points) + { + point.x *= scaleX; + point.y *= scaleY; + bboxMin.x = std::min(bboxMin.x, point.x); + bboxMin.y = std::min(bboxMin.y, point.y); + bboxMax.x = std::max(bboxMax.x, point.x); + bboxMax.y = std::max(bboxMax.y, point.y); + } + } + } + + bboxMin.x = std::floor(bboxMin.x); + bboxMin.y = std::floor(bboxMin.y); + + // Reposition glyph to bitmap so it begins at (0,0) for our bitmap + for (auto& subpath : path.subpaths) + { + for (auto& point : subpath.points) + { + point.x -= bboxMin.x; + point.y -= bboxMin.y; + } + } + + TrueTypeGlyph glyph; + + // Rasterize the glyph + glyph.width = (int)std::floor(bboxMax.x - bboxMin.x) + 1; + glyph.height = (int)std::floor(bboxMax.y - bboxMin.y) + 1; + glyph.grayscale.reset(new uint8_t[glyph.width * glyph.height]); + uint8_t* grayscale = glyph.grayscale.get(); + path.Rasterize(grayscale, glyph.width, glyph.height); + + // TBD: gridfit or not? + glyph.advanceWidth = (int)std::round(advanceWidth * scale * scaleX); + glyph.leftSideBearing = (int)std::round(lsb * scale * scaleX + bboxMin.x); + glyph.yOffset = (int)std::round(bboxMin.y); + + return glyph; + } + else if (numberOfContours < 0) // Composite glyph + { + ttf_uint16 flags = reader.ReadUInt16(); + ttf_uint16 glyphIndex = reader.ReadUInt16(); + + // To do: implement this + + return {}; + } + + return {}; +} + +uint32_t TrueTypeFont::GetGlyphIndex(uint32_t c) const +{ + auto it = std::lower_bound(Ranges.begin(), Ranges.end(), c, [](const TTF_GlyphRange& range, uint32_t c) { return range.endCharCode < c; }); + if (it != Ranges.end() && c >= it->startCharCode && c <= it->endCharCode) + { + return it->startGlyphID + (c - it->startCharCode); + } + + it = std::lower_bound(ManyToOneRanges.begin(), ManyToOneRanges.end(), c, [](const TTF_GlyphRange& range, uint32_t c) { return range.endCharCode < c; }); + if (it != ManyToOneRanges.end() && c >= it->startCharCode && c <= it->endCharCode) + { + return it->startGlyphID; + } + return 0; +} + +void TrueTypeFont::LoadCharacterMapEncoding(TrueTypeFileReader& reader) +{ + // Look for the best encoding available that we support + + TTF_EncodingRecord record; + if (!record.subtableOffset) record = cmap.GetEncoding(3, 12); + if (!record.subtableOffset) record = cmap.GetEncoding(0, 4); + if (!record.subtableOffset) record = cmap.GetEncoding(3, 1); + if (!record.subtableOffset) record = cmap.GetEncoding(0, 3); + if (!record.subtableOffset) + throw std::runtime_error("No supported cmap encoding found in truetype file"); + + reader.Seek(record.subtableOffset); + + ttf_uint16 format = reader.ReadUInt16(); + if (format == 4) + { + TTF_CMapSubtable4 subformat; + subformat.Load(reader); + + for (uint16_t i = 0; i < subformat.segCount; i++) + { + ttf_uint16 startCode = subformat.startCode[i]; + ttf_uint16 endCode = subformat.endCode[i]; + ttf_uint16 idDelta = subformat.idDelta[i]; + ttf_uint16 idRangeOffset = subformat.idRangeOffsets[i]; + if (idRangeOffset == 0) + { + ttf_uint16 glyphId = startCode + idDelta; // Note: relies on modulo 65536 + + TTF_GlyphRange range; + range.startCharCode = startCode; + range.endCharCode = endCode; + range.startGlyphID = glyphId; + Ranges.push_back(range); + } + else if (startCode <= endCode) + { + TTF_GlyphRange range; + range.startCharCode = startCode; + bool firstGlyph = true; + for (ttf_uint16 c = startCode; c <= endCode; c++) + { + int offset = idRangeOffset / 2 + (c - startCode) - ((int)subformat.segCount - i); + if (offset >= 0 && offset < subformat.glyphIdArray.size()) + { + int glyphId = subformat.glyphIdArray[offset]; + if (firstGlyph) + { + range.startGlyphID = glyphId; + firstGlyph = false; + } + else if (range.startGlyphID + (c - range.startCharCode) != glyphId) + { + range.endCharCode = c - 1; + Ranges.push_back(range); + range.startCharCode = c; + range.startGlyphID = glyphId; + } + } + } + if (!firstGlyph) + { + range.endCharCode = endCode; + Ranges.push_back(range); + } + } + } + } + else if (format == 12 || format == 3) + { + TTF_CMapSubtable12 subformat; + subformat.Load(reader); + Ranges = std::move(subformat.groups); + } + else if (format == 13 || format == 3) + { + TTF_CMapSubtable13 subformat; + subformat.Load(reader); + ManyToOneRanges = std::move(subformat.groups); + } +} + +///////////////////////////////////////////////////////////////////////////// + +void TTF_CMapSubtable0::Load(TrueTypeFileReader& reader) +{ + length = reader.ReadUInt16(); + language = reader.ReadUInt16(); + glyphIdArray.resize(256); + reader.Read(glyphIdArray.data(), glyphIdArray.size()); +} + +///////////////////////////////////////////////////////////////////////////// + +void TTF_CMapSubtable4::Load(TrueTypeFileReader& reader) +{ + length = reader.ReadUInt16(); + language = reader.ReadUInt16(); + + segCount = reader.ReadUInt16() / 2; + ttf_uint16 searchRange = reader.ReadUInt16(); + ttf_uint16 entrySelector = reader.ReadUInt16(); + ttf_uint16 rangeShift = reader.ReadUInt16(); + + endCode.reserve(segCount); + startCode.reserve(segCount); + idDelta.reserve(segCount); + idRangeOffsets.reserve(segCount); + for (ttf_uint16 i = 0; i < segCount; i++) endCode.push_back(reader.ReadUInt16()); + reservedPad = reader.ReadUInt16(); + for (ttf_uint16 i = 0; i < segCount; i++) startCode.push_back(reader.ReadUInt16()); + for (ttf_uint16 i = 0; i < segCount; i++) idDelta.push_back(reader.ReadInt16()); + for (ttf_uint16 i = 0; i < segCount; i++) idRangeOffsets.push_back(reader.ReadUInt16()); + + int glyphIdArraySize = ((int)length - (8 + (int)segCount * 4) * sizeof(ttf_uint16)) / 2; + if (glyphIdArraySize < 0) + throw std::runtime_error("Invalid TTF cmap subtable 4 length"); + glyphIdArray.reserve(glyphIdArraySize); + for (int i = 0; i < glyphIdArraySize; i++) glyphIdArray.push_back(reader.ReadUInt16()); +} + +///////////////////////////////////////////////////////////////////////////// + +void TTF_CMapSubtable12::Load(TrueTypeFileReader& reader) +{ + reserved = reader.ReadUInt16(); + length = reader.ReadUInt32(); + language = reader.ReadUInt32(); + numGroups = reader.ReadUInt32(); + for (ttf_uint32 i = 0; i < numGroups; i++) + { + TTF_GlyphRange range; + range.startCharCode = reader.ReadUInt32(); + range.endCharCode = reader.ReadUInt32(); + range.startGlyphID = reader.ReadUInt32(); + groups.push_back(range); + } +} + +///////////////////////////////////////////////////////////////////////////// + +void TTF_TableRecord::Load(TrueTypeFileReader& reader) +{ + tableTag = reader.ReadTag(); + checksum = reader.ReadUInt32(); + offset = reader.ReadOffset32(); + length = reader.ReadUInt32(); +} + +///////////////////////////////////////////////////////////////////////////// + +void TTF_TableDirectory::Load(TrueTypeFileReader& reader) +{ + sfntVersion = reader.ReadUInt32(); + numTables = reader.ReadUInt16(); + + // opentype spec says we can't use these for security reasons, so we pretend they never was part of the header + ttf_uint16 searchRange = reader.ReadUInt16(); + ttf_uint16 entrySelector = reader.ReadUInt16(); + ttf_uint16 rangeShift = reader.ReadUInt16(); + + for (ttf_uint16 i = 0; i < numTables; i++) + { + TTF_TableRecord record; + record.Load(reader); + tableRecords.push_back(record); + } +} + +///////////////////////////////////////////////////////////////////////////// + +void TTC_Header::Load(TrueTypeFileReader& reader) +{ + ttcTag = reader.ReadTag(); + majorVersion = reader.ReadUInt16(); + minorVersion = reader.ReadUInt16(); + + if (!(majorVersion == 1 && minorVersion == 0) && !(majorVersion == 2 && minorVersion == 0)) + throw std::runtime_error("Unsupported TTC header version"); + + numFonts = reader.ReadUInt32(); + for (ttf_uint16 i = 0; i < numFonts; i++) + { + tableDirectoryOffsets.push_back(reader.ReadOffset32()); + } + + if (majorVersion == 2 && minorVersion == 0) + { + dsigTag = reader.ReadUInt32(); + dsigLength = reader.ReadUInt32(); + dsigOffset = reader.ReadUInt32(); + } +} + +///////////////////////////////////////////////////////////////////////////// + +void TTF_CMap::Load(TrueTypeFileReader& reader) +{ + version = reader.ReadUInt16(); + numTables = reader.ReadUInt16(); + + for (ttf_uint16 i = 0; i < numTables; i++) + { + TTF_EncodingRecord record; + record.platformID = reader.ReadUInt16(); + record.encodingID = reader.ReadUInt16(); + record.subtableOffset = reader.ReadOffset32(); + encodingRecords.push_back(record); + } +} + +///////////////////////////////////////////////////////////////////////////// + +void TTF_FontHeader::Load(TrueTypeFileReader& reader) +{ + majorVersion = reader.ReadUInt16(); + minorVersion = reader.ReadUInt16(); + fontRevision = reader.ReadFixed(); + checksumAdjustment = reader.ReadUInt32(); + magicNumber = reader.ReadUInt32(); + flags = reader.ReadUInt16(); + unitsPerEm = reader.ReadUInt16(); + created = reader.ReadLONGDATETIME(); + modified = reader.ReadLONGDATETIME(); + xMin = reader.ReadInt16(); + yMin = reader.ReadInt16(); + xMax = reader.ReadInt16(); + yMax = reader.ReadInt16(); + macStyle = reader.ReadUInt16(); + lowestRecPPEM = reader.ReadUInt16(); + fontDirectionHint = reader.ReadInt16(); + indexToLocFormat = reader.ReadInt16(); + glyphDataFormat = reader.ReadInt16(); +} + +///////////////////////////////////////////////////////////////////////////// + +void TTF_HorizontalHeader::Load(TrueTypeFileReader& reader) +{ + majorVersion = reader.ReadUInt16(); + minorVersion = reader.ReadUInt16(); + ascender = reader.ReadFWORD(); + descender = reader.ReadFWORD(); + lineGap = reader.ReadFWORD(); + advanceWidthMax = reader.ReadUFWORD(); + minLeftSideBearing = reader.ReadFWORD(); + minRightSideBearing = reader.ReadFWORD(); + xMaxExtent = reader.ReadFWORD(); + caretSlopeRise = reader.ReadInt16(); + caretSlopeRun = reader.ReadInt16(); + caretOffset = reader.ReadInt16(); + reserved0 = reader.ReadInt16(); + reserved1 = reader.ReadInt16(); + reserved2 = reader.ReadInt16(); + reserved3 = reader.ReadInt16(); + metricDataFormat = reader.ReadInt16(); + numberOfHMetrics = reader.ReadUInt16(); +} + +///////////////////////////////////////////////////////////////////////////// + +void TTF_HorizontalMetrics::Load(const TTF_HorizontalHeader& hhea, const TTF_MaximumProfile& maxp, TrueTypeFileReader& reader) +{ + for (ttf_uint16 i = 0; i < hhea.numberOfHMetrics; i++) + { + longHorMetric metric; + metric.advanceWidth = reader.ReadUInt16(); + metric.lsb = reader.ReadInt16(); + hMetrics.push_back(metric); + } + + int count = (int)maxp.numGlyphs - (int)hhea.numberOfHMetrics; + if (count < 0) + throw std::runtime_error("Invalid TTF file"); + + for (int i = 0; i < count; i++) + { + leftSideBearings.push_back(reader.ReadInt16()); + } +} + +///////////////////////////////////////////////////////////////////////////// + +void TTF_MaximumProfile::Load(TrueTypeFileReader& reader) +{ + version = reader.ReadVersion16Dot16(); + numGlyphs = reader.ReadUInt16(); + + if (version == (1 << 16)) // v1 only + { + maxPoints = reader.ReadUInt16(); + maxContours = reader.ReadUInt16(); + maxCompositePoints = reader.ReadUInt16(); + maxCompositeContours = reader.ReadUInt16(); + maxZones = reader.ReadUInt16(); + maxTwilightPoints = reader.ReadUInt16(); + maxStorage = reader.ReadUInt16(); + maxFunctionDefs = reader.ReadUInt16(); + maxInstructionDefs = reader.ReadUInt16(); + maxStackElements = reader.ReadUInt16(); + maxSizeOfInstructions = reader.ReadUInt16(); + maxComponentElements = reader.ReadUInt16(); + maxComponentDepth = reader.ReadUInt16(); + } +} + +///////////////////////////////////////////////////////////////////////////// + +void TTF_NamingTable::Load(TrueTypeFileReader& reader) +{ + version = reader.ReadUInt16(); + count = reader.ReadUInt16(); + storageOffset = reader.ReadOffset16(); + for (ttf_uint16 i = 0; i < count; i++) + { + NameRecord record; + record.platformID = reader.ReadUInt16(); + record.encodingID = reader.ReadUInt16(); + record.languageID = reader.ReadUInt16(); + record.nameID = reader.ReadUInt16(); + record.length = reader.ReadUInt16(); + record.stringOffset = reader.ReadOffset16(); + nameRecord.push_back(record); + } + + if (version == 1) + { + langTagCount = reader.ReadUInt16(); + for (ttf_uint16 i = 0; i < langTagCount; i++) + { + LangTagRecord record; + ttf_uint16 length; + ttf_Offset16 langTagOffset; + langTagRecord.push_back(record); + } + } +} + +///////////////////////////////////////////////////////////////////////////// + +void TTF_OS2Windows::Load(TrueTypeFileReader& reader) +{ + version = reader.ReadUInt16(); + xAvgCharWidth = reader.ReadInt16(); + usWeightClass = reader.ReadUInt16(); + usWidthClass = reader.ReadUInt16(); + fsType = reader.ReadUInt16(); + ySubscriptXSize = reader.ReadInt16(); + ySubscriptYSize = reader.ReadInt16(); + ySubscriptXOffset = reader.ReadInt16(); + ySubscriptYOffset = reader.ReadInt16(); + ySuperscriptXSize = reader.ReadInt16(); + ySuperscriptYSize = reader.ReadInt16(); + ySuperscriptXOffset = reader.ReadInt16(); + ySuperscriptYOffset = reader.ReadInt16(); + yStrikeoutSize = reader.ReadInt16(); + yStrikeoutPosition = reader.ReadInt16(); + sFamilyClass = reader.ReadInt16(); + for (int i = 0; i < 10; i++) + { + panose[i] = reader.ReadUInt8(); + } + ulUnicodeRange1 = reader.ReadUInt32(); + ulUnicodeRange2 = reader.ReadUInt32(); + ulUnicodeRange3 = reader.ReadUInt32(); + ulUnicodeRange4 = reader.ReadUInt32(); + achVendID = reader.ReadTag(); + fsSelection = reader.ReadUInt16(); + usFirstCharIndex = reader.ReadUInt16(); + usLastCharIndex = reader.ReadUInt16(); + sTypoAscender = reader.ReadInt16(); + sTypoDescender = reader.ReadInt16(); + sTypoLineGap = reader.ReadInt16(); + if (!reader.IsEndOfData()) // may be missing in v0 due to a bug in Apple's TTF documentation + { + usWinAscent = reader.ReadUInt16(); + usWinDescent = reader.ReadUInt16(); + } + if (version >= 1) + { + ulCodePageRange1 = reader.ReadUInt32(); + ulCodePageRange2 = reader.ReadUInt32(); + } + if (version >= 2) + { + sxHeight = reader.ReadInt16(); + sCapHeight = reader.ReadInt16(); + usDefaultChar = reader.ReadUInt16(); + usBreakChar = reader.ReadUInt16(); + usMaxContext = reader.ReadUInt16(); + } + if (version >= 5) + { + usLowerOpticalPointSize = reader.ReadUInt16(); + usUpperOpticalPointSize = reader.ReadUInt16(); + } +} + +///////////////////////////////////////////////////////////////////////////// + +void TTF_IndexToLocation::Load(const TTF_FontHeader& head, const TTF_MaximumProfile& maxp, TrueTypeFileReader& reader) +{ + int count = (int)maxp.numGlyphs + 1; + if (head.indexToLocFormat == 0) + { + offsets.reserve(count); + for (int i = 0; i < count; i++) + { + offsets.push_back((ttf_Offset32)reader.ReadOffset16() * 2); + } + } + else + { + offsets.reserve(count); + for (int i = 0; i < count; i++) + { + offsets.push_back(reader.ReadOffset32()); + } + } +} + +///////////////////////////////////////////////////////////////////////////// + +ttf_uint8 TrueTypeFileReader::ReadUInt8() +{ + ttf_uint8 v; Read(&v, 1); return v; +} + +ttf_uint16 TrueTypeFileReader::ReadUInt16() +{ + ttf_uint8 v[2]; Read(v, 2); return (((ttf_uint16)v[0]) << 8) | (ttf_uint16)v[1]; +} + +ttf_uint24 TrueTypeFileReader::ReadUInt24() +{ + ttf_uint8 v[3]; Read(v, 3); return (((ttf_uint32)v[0]) << 16) | (((ttf_uint32)v[1]) << 8) | (ttf_uint32)v[2]; +} + +ttf_uint32 TrueTypeFileReader::ReadUInt32() +{ + ttf_uint8 v[4]; Read(v, 4); return (((ttf_uint32)v[0]) << 24) | (((ttf_uint32)v[1]) << 16) | (((ttf_uint32)v[2]) << 8) | (ttf_uint32)v[3]; +} + +ttf_int8 TrueTypeFileReader::ReadInt8() +{ + return ReadUInt8(); +} + +ttf_int16 TrueTypeFileReader::ReadInt16() +{ + return ReadUInt16(); +} + +ttf_int32 TrueTypeFileReader::ReadInt32() +{ + return ReadUInt32(); +} + +ttf_Fixed TrueTypeFileReader::ReadFixed() +{ + return ReadUInt32(); +} + +ttf_UFWORD TrueTypeFileReader::ReadUFWORD() +{ + return ReadUInt16(); +} + +ttf_FWORD TrueTypeFileReader::ReadFWORD() +{ + return ReadUInt16(); +} + +ttf_F2DOT14 TrueTypeFileReader::ReadF2DOT14() +{ + return ReadUInt16(); +} + +ttf_LONGDATETIME TrueTypeFileReader::ReadLONGDATETIME() +{ + ttf_uint8 v[8]; Read(v, 8); + return + (((ttf_LONGDATETIME)v[0]) << 56) | (((ttf_LONGDATETIME)v[1]) << 48) | (((ttf_LONGDATETIME)v[2]) << 40) | (((ttf_LONGDATETIME)v[3]) << 32) | + (((ttf_LONGDATETIME)v[4]) << 24) | (((ttf_LONGDATETIME)v[5]) << 16) | (((ttf_LONGDATETIME)v[6]) << 8) | (ttf_LONGDATETIME)v[7]; +} + +ttf_Tag TrueTypeFileReader::ReadTag() +{ + ttf_Tag v; Read(v.data(), v.size()); return v; +} + +ttf_Offset16 TrueTypeFileReader::ReadOffset16() +{ + return ReadUInt16(); +} + +ttf_Offset24 TrueTypeFileReader::ReadOffset24() +{ + return ReadUInt24(); +} + +ttf_Offset32 TrueTypeFileReader::ReadOffset32() +{ + return ReadUInt32(); +} + +ttf_Version16Dot16 TrueTypeFileReader::ReadVersion16Dot16() +{ + return ReadUInt32(); +} + +void TrueTypeFileReader::Seek(size_t newpos) +{ + if (newpos > size) + throw std::runtime_error("Invalid TTF file"); + + pos = newpos; +} + +void TrueTypeFileReader::Read(void* output, size_t count) +{ + if (pos + count > size) + throw std::runtime_error("Unexpected end of TTF file"); + memcpy(output, data + pos, count); + pos += count; +} diff --git a/src/core/truetypefont.h b/src/core/truetypefont.h new file mode 100644 index 0000000..dabc1bf --- /dev/null +++ b/src/core/truetypefont.h @@ -0,0 +1,453 @@ +#pragma once + +#include +#include +#include +#include +#include + +typedef uint8_t ttf_uint8; +typedef uint16_t ttf_uint16; +typedef uint32_t ttf_uint24; // 24-bit unsigned integer +typedef uint32_t ttf_uint32; + +typedef int8_t ttf_int8; +typedef int16_t ttf_int16; +typedef int32_t ttf_int32; + +typedef uint32_t ttf_Fixed; // 32-bit signed fixed-point number (16.16) +typedef uint16_t ttf_UFWORD; // uint16 that describes a quantity in font design units +typedef int16_t ttf_FWORD; // int16 that describes a quantity in font design units +typedef uint32_t ttf_F2DOT14; // 16-bit signed fixed number with the low 14 bits of fraction (2.14) +typedef uint64_t ttf_LONGDATETIME; // number of seconds since 12:00 midnight, January 1, 1904, UTC + +typedef std::array ttf_Tag; // 4 byte identifier + +typedef uint16_t ttf_Offset16; // Short offset to a table, same as uint16, NULL offset = 0x0000 +typedef uint32_t ttf_Offset24; // 24-bit offset to a table, same as uint24, NULL offset = 0x000000 +typedef uint32_t ttf_Offset32; // Long offset to a table, same as uint32, NULL offset = 0x00000000 + +typedef uint32_t ttf_Version16Dot16; // Packed 32-bit value with major and minor version numbers + +class TrueTypeFileReader +{ +public: + TrueTypeFileReader() = default; + TrueTypeFileReader(const void* data, size_t size) : data(static_cast(data)), size(size) { } + + bool IsEndOfData() const { return pos == size; } + + ttf_uint8 ReadUInt8(); + ttf_uint16 ReadUInt16(); + ttf_uint24 ReadUInt24(); + ttf_uint32 ReadUInt32(); + ttf_int8 ReadInt8(); + ttf_int16 ReadInt16(); + ttf_int32 ReadInt32(); + ttf_Fixed ReadFixed(); + ttf_UFWORD ReadUFWORD(); + ttf_FWORD ReadFWORD(); + ttf_F2DOT14 ReadF2DOT14(); + ttf_LONGDATETIME ReadLONGDATETIME(); + ttf_Tag ReadTag(); + ttf_Offset16 ReadOffset16(); + ttf_Offset24 ReadOffset24(); + ttf_Offset32 ReadOffset32(); + ttf_Version16Dot16 ReadVersion16Dot16(); + + void Seek(size_t newpos); + void Read(void* output, size_t count); + +private: + const uint8_t* data = nullptr; + size_t size = 0; + size_t pos = 0; +}; + +struct TTF_TableRecord +{ + ttf_Tag tableTag = {}; + ttf_uint32 checksum = {}; + ttf_Offset32 offset = {}; + ttf_uint32 length = {}; + + void Load(TrueTypeFileReader& reader); + + TrueTypeFileReader GetReader(const void* filedata, size_t filesize) const + { + if ((size_t)offset + length > filesize) + throw std::runtime_error("Invalid TTF table directory record"); + + return TrueTypeFileReader((uint8_t*)filedata + offset, length); + } +}; + +struct TTF_TableDirectory +{ + ttf_uint32 sfntVersion = {}; + ttf_uint16 numTables = {}; + std::vector tableRecords; + + // To do: Apple TTF fonts allow 'true' and 'typ1' for sfntVersion as well. + bool ContainsTTFOutlines() const { return sfntVersion == 0x00010000; } + bool ContainsCFFData() const { return sfntVersion == 0x4F54544F; } + + void Load(TrueTypeFileReader& reader); + + const TTF_TableRecord& GetRecord(const char* tag) const + { + for (const auto& record : tableRecords) + { + if (memcmp(record.tableTag.data(), tag, 4) == 0) + { + return record; + } + } + throw std::runtime_error(std::string("Could not find required '") + tag + "' table entry"); + } + + TrueTypeFileReader GetReader(const void* filedata, size_t filesize, const char* tag) const + { + return GetRecord(tag).GetReader(filedata, filesize); + } +}; + +struct TTC_Header +{ + ttf_Tag ttcTag = {}; + ttf_uint16 majorVersion = {}; + ttf_uint16 minorVersion = {}; + ttf_uint32 numFonts = {}; + std::vector tableDirectoryOffsets; + + // majorVersion = 2, minorVersion = 0: + ttf_uint32 dsigTag = {}; + ttf_uint32 dsigLength = {}; + ttf_uint32 dsigOffset = {}; + + void Load(TrueTypeFileReader& reader); +}; + +struct TTF_EncodingRecord +{ + ttf_uint16 platformID = {}; + ttf_uint16 encodingID = {}; + ttf_Offset32 subtableOffset = {}; +}; + +struct TTF_CMap // 'cmap' Character to glyph mapping +{ + ttf_uint16 version = {}; + ttf_uint16 numTables = {}; + std::vector encodingRecords; // [numTables] + + void Load(TrueTypeFileReader& reader); + + TTF_EncodingRecord GetEncoding(ttf_uint16 platformID, ttf_uint16 encodingID) const + { + for (const TTF_EncodingRecord& record : encodingRecords) + { + if (record.platformID == platformID && record.encodingID == encodingID) + { + return record; + } + } + return {}; + } +}; + +struct TTF_GlyphRange +{ + ttf_uint32 startCharCode = 0; + ttf_uint32 endCharCode = 0; + ttf_uint32 startGlyphID = 0; +}; + +struct TTF_CMapSubtable0 // Byte encoding table +{ + ttf_uint16 length = {}; + ttf_uint16 language = {}; + std::vector glyphIdArray; + + void Load(TrueTypeFileReader& reader); +}; + +struct TTF_CMapSubtable4 // Segment mapping to delta values (U+0000 to U+FFFF) +{ + ttf_uint16 length = {}; + ttf_uint16 language = {}; + ttf_uint16 segCount = {}; + std::vector endCode; + ttf_uint16 reservedPad = {}; + std::vector startCode; + std::vector idDelta; + std::vector idRangeOffsets; + std::vector glyphIdArray; + + void Load(TrueTypeFileReader& reader); +}; + +struct TTF_CMapSubtable12 // Segmented coverage (U+0000 to U+10FFFF) +{ + ttf_uint16 reserved; + ttf_uint32 length; + ttf_uint32 language; + ttf_uint32 numGroups; + std::vector groups; + + void Load(TrueTypeFileReader& reader); +}; + +typedef TTF_CMapSubtable12 TTF_CMapSubtable13; // Many-to-one range mappings + +struct TTF_FontHeader // 'head' Font header +{ + ttf_uint16 majorVersion = {}; + ttf_uint16 minorVersion = {}; + ttf_Fixed fontRevision = {}; + ttf_uint32 checksumAdjustment = {}; + ttf_uint32 magicNumber = {}; + ttf_uint16 flags = {}; + ttf_uint16 unitsPerEm = {}; + ttf_LONGDATETIME created = {}; + ttf_LONGDATETIME modified = {}; + ttf_int16 xMin = {}; + ttf_int16 yMin = {}; + ttf_int16 xMax = {}; + ttf_int16 yMax = {}; + ttf_uint16 macStyle = {}; + ttf_uint16 lowestRecPPEM = {}; + ttf_int16 fontDirectionHint = {}; + ttf_int16 indexToLocFormat = {}; + ttf_int16 glyphDataFormat = {}; + + void Load(TrueTypeFileReader& reader); +}; + +struct TTF_HorizontalHeader // 'hhea' Horizontal header +{ + ttf_uint16 majorVersion = {}; + ttf_uint16 minorVersion = {}; + ttf_FWORD ascender = {}; + ttf_FWORD descender = {}; + ttf_FWORD lineGap = {}; + ttf_UFWORD advanceWidthMax = {}; + ttf_FWORD minLeftSideBearing = {}; + ttf_FWORD minRightSideBearing = {}; + ttf_FWORD xMaxExtent = {}; + ttf_int16 caretSlopeRise = {}; + ttf_int16 caretSlopeRun = {}; + ttf_int16 caretOffset = {}; + ttf_int16 reserved0 = {}; + ttf_int16 reserved1 = {}; + ttf_int16 reserved2 = {}; + ttf_int16 reserved3 = {}; + ttf_int16 metricDataFormat = {}; + ttf_uint16 numberOfHMetrics = {}; + + void Load(TrueTypeFileReader& reader); +}; + +struct TTF_MaximumProfile; + +struct TTF_HorizontalMetrics // 'hmtx' Horizontal metrics +{ + struct longHorMetric + { + ttf_uint16 advanceWidth = {}; + ttf_int16 lsb = {}; + }; + std::vector hMetrics; // [hhea.numberOfHMetrics] + std::vector leftSideBearings; // [maxp.numGlyphs - hhea.numberOfHMetrics] + + void Load(const TTF_HorizontalHeader& hhea, const TTF_MaximumProfile& maxp, TrueTypeFileReader& reader); +}; + +struct TTF_MaximumProfile // 'maxp' Maximum profile +{ + // v0.5 and v1: + ttf_Version16Dot16 version = {}; + ttf_uint16 numGlyphs = {}; + + // v1 only: + ttf_uint16 maxPoints = {}; + ttf_uint16 maxContours = {}; + ttf_uint16 maxCompositePoints = {}; + ttf_uint16 maxCompositeContours = {}; + ttf_uint16 maxZones = {}; + ttf_uint16 maxTwilightPoints = {}; + ttf_uint16 maxStorage = {}; + ttf_uint16 maxFunctionDefs = {}; + ttf_uint16 maxInstructionDefs = {}; + ttf_uint16 maxStackElements = {}; + ttf_uint16 maxSizeOfInstructions = {}; + ttf_uint16 maxComponentElements = {}; + ttf_uint16 maxComponentDepth = {}; + + void Load(TrueTypeFileReader& reader); +}; + +struct TTF_NamingTable // 'name' Naming table +{ + struct NameRecord + { + ttf_uint16 platformID = {}; + ttf_uint16 encodingID = {}; + ttf_uint16 languageID = {}; + ttf_uint16 nameID = {}; + ttf_uint16 length = {}; + ttf_Offset16 stringOffset = {}; + }; + + struct LangTagRecord + { + ttf_uint16 length = {}; + ttf_Offset16 langTagOffset = {}; + }; + + // v0 and v1: + ttf_uint16 version = {}; + ttf_uint16 count = {}; + ttf_Offset16 storageOffset = {}; + std::vector nameRecord; // [count] + + // v1 only: + ttf_uint16 langTagCount = {}; + std::vector langTagRecord; // [langTagCount] + + void Load(TrueTypeFileReader& reader); +}; + +struct TTF_OS2Windows // 'OS/2' Windows specific metrics +{ + ttf_uint16 version = {}; + ttf_int16 xAvgCharWidth = {}; + ttf_uint16 usWeightClass = {}; + ttf_uint16 usWidthClass = {}; + ttf_uint16 fsType = {}; + ttf_int16 ySubscriptXSize = {}; + ttf_int16 ySubscriptYSize = {}; + ttf_int16 ySubscriptXOffset = {}; + ttf_int16 ySubscriptYOffset = {}; + ttf_int16 ySuperscriptXSize = {}; + ttf_int16 ySuperscriptYSize = {}; + ttf_int16 ySuperscriptXOffset = {}; + ttf_int16 ySuperscriptYOffset = {}; + ttf_int16 yStrikeoutSize = {}; + ttf_int16 yStrikeoutPosition = {}; + ttf_int16 sFamilyClass = {}; + ttf_uint8 panose[10] = {}; + ttf_uint32 ulUnicodeRange1 = {}; + ttf_uint32 ulUnicodeRange2 = {}; + ttf_uint32 ulUnicodeRange3 = {}; + ttf_uint32 ulUnicodeRange4 = {}; + ttf_Tag achVendID = {}; + ttf_uint16 fsSelection = {}; + ttf_uint16 usFirstCharIndex = {}; + ttf_uint16 usLastCharIndex = {}; + ttf_int16 sTypoAscender = {}; + ttf_int16 sTypoDescender = {}; + ttf_int16 sTypoLineGap = {}; + ttf_uint16 usWinAscent = {}; // may be missing in v0 due to bugs in Apple docs + ttf_uint16 usWinDescent = {}; // may be missing in v0 due to bugs in Apple docs + ttf_uint32 ulCodePageRange1 = {}; // v1 + ttf_uint32 ulCodePageRange2 = {}; + ttf_int16 sxHeight = {}; // v2, v3 and v4 + ttf_int16 sCapHeight = {}; + ttf_uint16 usDefaultChar = {}; + ttf_uint16 usBreakChar = {}; + ttf_uint16 usMaxContext = {}; + ttf_uint16 usLowerOpticalPointSize = {}; // v5 + ttf_uint16 usUpperOpticalPointSize = {}; + + void Load(TrueTypeFileReader& reader); +}; + +// Simple glyph flags: +#define TTF_ON_CURVE_POINT 0x01 +#define TTF_X_SHORT_VECTOR 0x02 +#define TTF_Y_SHORT_VECTOR 0x04 +#define TTF_REPEAT_FLAG 0x08 +#define TTF_X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR 0x10 +#define TTF_Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR 0x20 +#define TTF_OVERLAP_SIMPLE = 0x40 + +// Composite glyph flags: +#define TTF_ARG_1_AND_2_ARE_WORDS 0x0001 +#define TTF_ARGS_ARE_XY_VALUES 0x0002 +#define TTF_ROUND_XY_TO_GRID 0x0004 +#define TTF_WE_HAVE_A_SCALE 0x0008 +#define TTF_MORE_COMPONENTS 0x0020 +#define TTF_WE_HAVE_AN_X_AND_Y_SCALE 0x0040 +#define TTF_WE_HAVE_A_TWO_BY_TWO 0x0080 +#define TTF_WE_HAVE_INSTRUCTIONS 0x0100 +#define TTF_USE_MY_METRICS 0x0200 +#define TTF_OVERLAP_COMPOUND 0x0400 +#define TTF_SCALED_COMPONENT_OFFSET 0x0800 +#define TTF_UNSCALED_COMPONENT_OFFSET 0x1000 + +struct TTF_IndexToLocation // 'loca' Index to location +{ + std::vector offsets; + + void Load(const TTF_FontHeader& head, const TTF_MaximumProfile& maxp, TrueTypeFileReader& reader); +}; + +struct TTF_Point +{ + ttf_int16 x; + ttf_int16 y; +}; + +class TrueTypeGlyph +{ +public: + int advanceWidth = 0; + int leftSideBearing = 0; + int yOffset = 0; + + int width = 0; + int height = 0; + std::unique_ptr grayscale; +}; + +class TrueTypeTextMetrics +{ +public: + double ascender = 0.0; + double descender = 0.0; + double lineGap = 0.0; +}; + +class TrueTypeFont +{ +public: + TrueTypeFont(std::vector data); + + TrueTypeTextMetrics GetTextMetrics(double height) const; + uint32_t GetGlyphIndex(uint32_t codepoint) const; + TrueTypeGlyph LoadGlyph(uint32_t glyphIndex, double height) const; + +private: + void LoadCharacterMapEncoding(TrueTypeFileReader& reader); + + std::vector data; + + TTF_TableDirectory directory; + + // Required for all OpenType fonts: + TTF_CMap cmap; + TTF_FontHeader head; + TTF_HorizontalHeader hhea; + TTF_HorizontalMetrics hmtx; + TTF_MaximumProfile maxp; + TTF_NamingTable name; + TTF_OS2Windows os2; + + // TrueType outlines: + TTF_TableRecord glyf; // Parsed on a per glyph basis using offsets from other tables + TTF_IndexToLocation loca; + + std::vector Ranges; + std::vector ManyToOneRanges; +}; diff --git a/src/core/widget.cpp b/src/core/widget.cpp index 9a1082f..9c6dc63 100644 --- a/src/core/widget.cpp +++ b/src/core/widget.cpp @@ -367,14 +367,38 @@ void Widget::UnlockCursor() void Widget::SetCursor(StandardCursor cursor) { + if (CurrentCursor != cursor) + { + CurrentCursor = cursor; + if (HoverWidget == this || CaptureWidget == this) + { + Widget* w = Window(); + if (w) + { + w->DispWindow->SetCursor(CurrentCursor); + } + } + } } void Widget::CaptureMouse() { + Widget* w = Window(); + if (w && w->CaptureWidget != this) + { + w->CaptureWidget = this; + w->DispWindow->CaptureMouse(); + } } void Widget::ReleaseMouseCapture() { + Widget* w = Window(); + if (w && w->CaptureWidget != nullptr) + { + w->CaptureWidget = nullptr; + w->DispWindow->ReleaseMouseCapture(); + } } std::string Widget::GetClipboardText() @@ -419,7 +443,7 @@ Widget* Widget::ChildAt(const Point& pos) { if (cur->FrameGeometry.contains(pos)) { - Widget* cur2 = cur->ChildAt(pos - cur->FrameGeometry.topLeft()); + Widget* cur2 = cur->ChildAt(pos - cur->ContentGeometry.topLeft()); return cur2 ? cur2 : cur; } } @@ -487,6 +511,7 @@ void Widget::OnWindowMouseMove(const Point& pos) { if (CaptureWidget) { + DispWindow->SetCursor(CaptureWidget->CurrentCursor); CaptureWidget->OnMouseMove(CaptureWidget->MapFrom(this, pos)); } else @@ -502,6 +527,7 @@ void Widget::OnWindowMouseMove(const Point& pos) HoverWidget = widget; } + DispWindow->SetCursor(widget->CurrentCursor); widget->OnMouseMove(widget->MapFrom(this, pos)); } } diff --git a/src/widgets/lineedit/lineedit.cpp b/src/widgets/lineedit/lineedit.cpp index 0d4c373..bab04c3 100644 --- a/src/widgets/lineedit/lineedit.cpp +++ b/src/widgets/lineedit/lineedit.cpp @@ -1066,8 +1066,8 @@ void LineEdit::OnPaintFrame(Canvas* canvas) { double w = GetFrameGeometry().width; double h = GetFrameGeometry().height; - Colorf bordercolor = Colorf::fromRgba8(200, 200, 200); - canvas->fillRect(Rect::xywh(0.0, 0.0, w, h), Colorf::fromRgba8(255, 255, 255)); + Colorf bordercolor = Colorf::fromRgba8(100, 100, 100); + canvas->fillRect(Rect::xywh(0.0, 0.0, w, h), Colorf::fromRgba8(38, 38, 38)); canvas->fillRect(Rect::xywh(0.0, 0.0, w, 1.0), bordercolor); canvas->fillRect(Rect::xywh(0.0, h - 1.0, w, 1.0), bordercolor); canvas->fillRect(Rect::xywh(0.0, 0.0, 1.0, h - 0.0), bordercolor); @@ -1096,21 +1096,21 @@ void LineEdit::OnPaint(Canvas* canvas) { // Draw selection box. Rect selection_rect = GetSelectionRect(); - canvas->fillRect(selection_rect, HasFocus() ? Colorf::fromRgba8(153, 201, 239) : Colorf::fromRgba8(229, 235, 241)); + canvas->fillRect(selection_rect, HasFocus() ? Colorf::fromRgba8(100, 100, 100) : Colorf::fromRgba8(68, 68, 68)); } // Draw text before selection if (!txt_before.empty()) { - canvas->drawText(Point(0.0, canvas->verticalTextAlign().baseline), Colorf::fromRgba8(0, 0, 0), txt_before); + canvas->drawText(Point(0.0, canvas->verticalTextAlign().baseline), Colorf::fromRgba8(255, 255, 255), txt_before); } if (!txt_selected.empty()) { - canvas->drawText(Point(size_before.width, canvas->verticalTextAlign().baseline), Colorf::fromRgba8(0, 0, 0), txt_selected); + canvas->drawText(Point(size_before.width, canvas->verticalTextAlign().baseline), Colorf::fromRgba8(255, 255, 255), txt_selected); } if (!txt_after.empty()) { - canvas->drawText(Point(size_before.width + size_selected.width, canvas->verticalTextAlign().baseline), Colorf::fromRgba8(0, 0, 0), txt_after); + canvas->drawText(Point(size_before.width + size_selected.width, canvas->verticalTextAlign().baseline), Colorf::fromRgba8(255, 255, 255), txt_after); } // draw cursor @@ -1119,7 +1119,7 @@ void LineEdit::OnPaint(Canvas* canvas) if (cursor_blink_visible) { Rect cursor_rect = GetCursorRect(); - canvas->fillRect(cursor_rect, Colorf::fromRgba8(0, 0, 0)); + canvas->fillRect(cursor_rect, Colorf::fromRgba8(255, 255, 255)); } } } diff --git a/src/widgets/listview/listview.cpp b/src/widgets/listview/listview.cpp index 6093d9d..fe73ebd 100644 --- a/src/widgets/listview/listview.cpp +++ b/src/widgets/listview/listview.cpp @@ -1,9 +1,13 @@ #include "widgets/listview/listview.h" +#include "widgets/scrollbar/scrollbar.h" ListView::ListView(Widget* parent) : Widget(parent) { - SetNoncontentSizes(10.0, 5.0, 10.0, 5.0); + SetNoncontentSizes(10.0, 10.0, 3.0, 10.0); + + scrollbar = new Scrollbar(this); + scrollbar->FuncScroll = [=]() { OnScrollbarScroll(); }; } void ListView::AddItem(const std::string& text) @@ -18,21 +22,62 @@ void ListView::Activate() OnActivated(); } +void ListView::SetSelectedItem(int index) +{ + if (selectedItem != index && index >= 0 && index < items.size()) + { + selectedItem = index; + Update(); + } +} + +void ListView::ScrollToItem(int index) +{ + double itemHeight = 20.0; + double y = itemHeight * index; + if (y < scrollbar->GetPosition()) + { + scrollbar->SetPosition(y); + } + else if (y + itemHeight > scrollbar->GetPosition() + GetHeight()) + { + scrollbar->SetPosition(std::max(y + itemHeight - GetHeight(), 0.0)); + } +} + +void ListView::OnScrollbarScroll() +{ + Update(); +} + +void ListView::OnGeometryChanged() +{ + double w = GetWidth(); + double h = GetHeight(); + double sw = scrollbar->GetPreferredWidth(); + scrollbar->SetFrameGeometry(Rect::xywh(w - sw, 0.0, sw, h)); + scrollbar->SetRanges(h, items.size() * 20.0); +} + void ListView::OnPaint(Canvas* canvas) { - double y = 20.0; + double y = -scrollbar->GetPosition(); double x = 2.0; - double w = GetFrameGeometry().width; + double w = GetWidth() - scrollbar->GetPreferredWidth() - 2.0; double h = 20.0; int index = 0; for (const std::string& item : items) { - if (index == selectedItem) + double itemY = y; + if (itemY + h >= 0.0 && itemY < GetHeight()) { - canvas->fillRect(Rect::xywh(x - 2.0, y + 5.0 - h, w, h), Colorf::fromRgba8(100, 100, 100)); + if (index == selectedItem) + { + canvas->fillRect(Rect::xywh(x - 2.0, itemY, w, h), Colorf::fromRgba8(100, 100, 100)); + } + canvas->drawText(Point(x, y + 15.0), Colorf::fromRgba8(255, 255, 255), item); } - canvas->drawText(Point(x, y), Colorf::fromRgba8(255, 255, 255), item); y += h; index++; } @@ -56,11 +101,11 @@ void ListView::OnMouseDown(const Point& pos, int key) if (key == IK_LeftMouse) { - int index = (int)((pos.y - 5.0) / 20.0); + int index = (int)((pos.y - 5.0 + scrollbar->GetPosition()) / 20.0); if (index >= 0 && (size_t)index < items.size()) { - selectedItem = index; - Update(); + SetSelectedItem(index); + ScrollToItem(selectedItem); } } } @@ -73,23 +118,35 @@ void ListView::OnMouseDoubleclick(const Point& pos, int key) } } +void ListView::OnMouseWheel(const Point& pos, EInputKey key) +{ + if (key == IK_MouseWheelUp) + { + scrollbar->SetPosition(std::max(scrollbar->GetPosition() - 20.0, 0.0)); + } + else if (key == IK_MouseWheelDown) + { + scrollbar->SetPosition(std::max(scrollbar->GetPosition() + 20.0, scrollbar->GetMax())); + } +} + void ListView::OnKeyDown(EInputKey key) { if (key == IK_Down) { if (selectedItem + 1 < (int)items.size()) { - selectedItem++; - Update(); + SetSelectedItem(selectedItem + 1); } + ScrollToItem(selectedItem); } else if (key == IK_Up) { if (selectedItem > 0) { - selectedItem--; - Update(); + SetSelectedItem(selectedItem - 1); } + ScrollToItem(selectedItem); } else if (key == IK_Enter) { diff --git a/src/widgets/scrollbar/scrollbar.cpp b/src/widgets/scrollbar/scrollbar.cpp index eea72fe..1462981 100644 --- a/src/widgets/scrollbar/scrollbar.cpp +++ b/src/widgets/scrollbar/scrollbar.cpp @@ -25,27 +25,27 @@ bool Scrollbar::IsHorizontal() const return !vertical; } -int Scrollbar::GetMin() const +double Scrollbar::GetMin() const { return scroll_min; } -int Scrollbar::GetMax() const +double Scrollbar::GetMax() const { return scroll_max; } -int Scrollbar::GetLineStep() const +double Scrollbar::GetLineStep() const { return line_step; } -int Scrollbar::GetPageStep() const +double Scrollbar::GetPageStep() const { return page_step; } -int Scrollbar::GetPosition() const +double Scrollbar::GetPosition() const { return position; } @@ -64,61 +64,61 @@ void Scrollbar::SetHorizontal() Update(); } -void Scrollbar::SetMin(int new_scroll_min) +void Scrollbar::SetMin(double new_scroll_min) { SetRanges(new_scroll_min, scroll_max, line_step, page_step); } -void Scrollbar::SetMax(int new_scroll_max) +void Scrollbar::SetMax(double new_scroll_max) { SetRanges(scroll_min, new_scroll_max, line_step, page_step); } -void Scrollbar::SetLineStep(int step) +void Scrollbar::SetLineStep(double step) { SetRanges(scroll_min, scroll_max, step, page_step); } -void Scrollbar::SetPageStep(int step) +void Scrollbar::SetPageStep(double step) { SetRanges(scroll_min, scroll_max, line_step, step); } -void Scrollbar::SetRanges(int scroll_min, int scroll_max, int line_step, int page_step) +void Scrollbar::SetRanges(double new_scroll_min, double new_scroll_max, double new_line_step, double new_page_step) { - if (scroll_min >= scroll_max || line_step <= 0 || page_step <= 0) + if (new_scroll_min >= new_scroll_max || new_line_step <= 0.0 || new_page_step <= 0.0) throw std::runtime_error("Scrollbar ranges out of bounds!"); - scroll_min = scroll_min; - scroll_max = scroll_max; - line_step = line_step; - page_step = page_step; + scroll_min = new_scroll_min; + scroll_max = new_scroll_max; + line_step = new_line_step; + page_step = new_page_step; if (position >= scroll_max) - position = scroll_max - 1; + position = scroll_max - 1.0; if (position < scroll_min) position = scroll_min; if (UpdatePartPositions()) Update(); } -void Scrollbar::SetRanges(int view_size, int total_size) +void Scrollbar::SetRanges(double view_size, double total_size) { - if (view_size <= 0 || total_size <= 0) + if (view_size <= 0.0 || total_size <= 0.0) { - SetRanges(0, 1, 1, 1); + SetRanges(0.0, 1.0, 1.0, 1.0); } else { - int scroll_max = std::max(1, total_size - view_size + 1); - int page_step = std::max(1, view_size); - SetRanges(0, scroll_max, 1, page_step); + double scroll_max = std::max(1.0, total_size - view_size + 1.0); + double page_step = std::max(1.0, view_size); + SetRanges(0.0, scroll_max, 10, page_step); } } -void Scrollbar::SetPosition(int pos) +void Scrollbar::SetPosition(double pos) { position = pos; if (pos >= scroll_max) - position = scroll_max - 1; + position = scroll_max - 1.0; if (pos < scroll_min) position = scroll_min; @@ -130,24 +130,24 @@ void Scrollbar::OnMouseMove(const Point& pos) { if (mouse_down_mode == mouse_down_thumb_drag) { - int last_position = position; + double last_position = position; - if (pos.x < -100 || pos.x > GetWidth() + 100 || pos.y < -100 || pos.y > GetHeight() + 100) + if (pos.x < -100.0 || pos.x > GetWidth() + 100.0 || pos.y < -100.0 || pos.y > GetHeight() + 100.0) { position = thumb_start_position; } else { - int delta = (int)(vertical ? (pos.y - mouse_drag_start_pos.y) : (pos.x - mouse_drag_start_pos.x)); - int position_pixels = thumb_start_pixel_position + delta; + double delta = vertical ? (pos.y - mouse_drag_start_pos.y) : (pos.x - mouse_drag_start_pos.x); + double position_pixels = thumb_start_pixel_position + delta; - int track_height = 0; + double track_height = 0; if (vertical) - track_height = (int)(rect_track_decrement.height + rect_track_increment.height); + track_height = rect_track_decrement.height + rect_track_increment.height; else - track_height = (int)(rect_track_decrement.width + rect_track_increment.width); + track_height = rect_track_decrement.width + rect_track_increment.width; - if (track_height != 0) + if (track_height != 0.0) position = scroll_min + position_pixels * (scroll_max - scroll_min) / track_height; else position = 0; @@ -178,12 +178,12 @@ void Scrollbar::OnMouseDown(const Point& pos, int key) mouse_down_mode = mouse_down_button_decr; FuncScrollOnMouseDown = &FuncScrollLineDecrement; - int last_position = position; + double last_position = position; position -= line_step; last_step_size = -line_step; if (position >= scroll_max) - position = scroll_max - 1; + position = scroll_max - 1.0; if (position < scroll_min) position = scroll_min; @@ -195,12 +195,12 @@ void Scrollbar::OnMouseDown(const Point& pos, int key) mouse_down_mode = mouse_down_button_incr; FuncScrollOnMouseDown = &FuncScrollLineIncrement; - int last_position = position; + double last_position = position; position += line_step; last_step_size = line_step; if (position >= scroll_max) - position = scroll_max - 1; + position = scroll_max - 1.0; if (position < scroll_min) position = scroll_min; @@ -211,19 +211,19 @@ void Scrollbar::OnMouseDown(const Point& pos, int key) { mouse_down_mode = mouse_down_thumb_drag; thumb_start_position = position; - thumb_start_pixel_position = (int)(vertical ? (rect_thumb.y - rect_track_decrement.y) : (rect_thumb.x - rect_track_decrement.x)); + thumb_start_pixel_position = vertical ? (rect_thumb.y - rect_track_decrement.y) : (rect_thumb.x - rect_track_decrement.x); } else if (rect_track_decrement.contains(pos)) { mouse_down_mode = mouse_down_track_decr; FuncScrollOnMouseDown = &FuncScrollPageDecrement; - int last_position = position; + double last_position = position; position -= page_step; last_step_size = -page_step; if (position >= scroll_max) - position = scroll_max - 1; + position = scroll_max - 1.0; if (position < scroll_min) position = scroll_min; @@ -235,12 +235,12 @@ void Scrollbar::OnMouseDown(const Point& pos, int key) mouse_down_mode = mouse_down_track_incr; FuncScrollOnMouseDown = &FuncScrollPageIncrement; - int last_position = position; + double last_position = position; position += page_step; last_step_size = page_step; if (position >= scroll_max) - position = scroll_max - 1; + position = scroll_max - 1.0; if (position < scroll_min) position = scroll_min; @@ -291,20 +291,26 @@ void Scrollbar::OnPaint(Canvas* canvas) part_track_increment.render_box(canvas, rect_track_increment); part_button_increment.render_box(canvas, rect_button_increment); */ + + canvas->fillRect(Rect::shrink(Rect::xywh(0.0, 0.0, GetWidth(), GetHeight()), 4.0, 0.0, 4.0, 0.0), Colorf::fromRgba8(33, 33, 33)); + canvas->fillRect(Rect::shrink(rect_thumb, 4.0, 0.0, 4.0, 0.0), Colorf::fromRgba8(58, 58, 58)); } // Calculates positions of all parts. Returns true if thumb position was changed compared to previously, false otherwise. bool Scrollbar::UpdatePartPositions() { - int total_height = (int)(vertical ? GetHeight() : GetWidth()); - int track_height = std::max(0, total_height - decr_height - incr_height); - int thumb_height = CalculateThumbSize(track_height); + double decr_height = showbuttons ? 16.0 : 0.0; + double incr_height = showbuttons ? 16.0 : 0.0; + + double total_height = vertical ? GetHeight() : GetWidth(); + double track_height = std::max(0.0, total_height - decr_height - incr_height); + double thumb_height = CalculateThumbSize(track_height); - int thumb_offset = decr_height + CalculateThumbPosition(thumb_height, track_height); + double thumb_offset = decr_height + CalculateThumbPosition(thumb_height, track_height); Rect previous_rect_thumb = rect_thumb; - rect_button_decrement = CreateRect(0, decr_height); + rect_button_decrement = CreateRect(0.0, decr_height); rect_track_decrement = CreateRect(decr_height, thumb_offset); rect_thumb = CreateRect(thumb_offset, thumb_offset + thumb_height); rect_track_increment = CreateRect(thumb_offset + thumb_height, decr_height + track_height); @@ -313,12 +319,12 @@ bool Scrollbar::UpdatePartPositions() return (previous_rect_thumb != rect_thumb); } -int Scrollbar::CalculateThumbSize(int track_size) +double Scrollbar::CalculateThumbSize(double track_size) { - int minimum_thumb_size = 20; - int range = scroll_max - scroll_min; - int length = range + page_step - 1; - int thumb_size = page_step * track_size / length; + double minimum_thumb_size = 20.0; + double range = scroll_max - scroll_min; + double length = range + page_step - 1; + double thumb_size = page_step * track_size / length; if (thumb_size < minimum_thumb_size) thumb_size = minimum_thumb_size; if (thumb_size > track_size) @@ -326,13 +332,13 @@ int Scrollbar::CalculateThumbSize(int track_size) return thumb_size; } -int Scrollbar::CalculateThumbPosition(int thumb_size, int track_size) +double Scrollbar::CalculateThumbPosition(double thumb_size, double track_size) { - int relative_pos = position - scroll_min; - int range = scroll_max - scroll_min - 1; + double relative_pos = position - scroll_min; + double range = scroll_max - scroll_min - 1; if (range != 0) { - int available_area = std::max(0, track_size - thumb_size); + double available_area = std::max(0.0, track_size - thumb_size); return relative_pos * available_area / range; } else @@ -341,7 +347,7 @@ int Scrollbar::CalculateThumbPosition(int thumb_size, int track_size) } } -Rect Scrollbar::CreateRect(int start, int end) +Rect Scrollbar::CreateRect(double start, double end) { if (vertical) return Rect(0.0, start, GetWidth(), end - start); @@ -356,7 +362,7 @@ void Scrollbar::OnTimerExpired() mouse_down_timer->Start(100, false); - int last_position = position; + double last_position = position; position += last_step_size; if (position >= scroll_max) position = scroll_max - 1; @@ -395,6 +401,6 @@ void Scrollbar::InvokeScrollEvent(std::function* event_ptr) if (FuncScroll) FuncScroll(); - if (event_ptr) + if (event_ptr && *event_ptr) (*event_ptr)(); } diff --git a/src/window/sdl2/sdl2displaywindow.cpp b/src/window/sdl2/sdl2displaywindow.cpp new file mode 100644 index 0000000..2cb52e2 --- /dev/null +++ b/src/window/sdl2/sdl2displaywindow.cpp @@ -0,0 +1,676 @@ + +#include "sdl2displaywindow.h" +#include + +Uint32 SDL2DisplayWindow::PaintEventNumber = 0xffffffff; +bool SDL2DisplayWindow::ExitRunLoop; +std::unordered_map SDL2DisplayWindow::WindowList; + +class InitSDL +{ +public: + InitSDL() + { + int result = SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS); + if (result != 0) + throw std::runtime_error(std::string("Unable to initialize SDL:") + SDL_GetError()); + + SDL2DisplayWindow::PaintEventNumber = SDL_RegisterEvents(1); + } +}; + +static void CheckInitSDL() +{ + static InitSDL initsdl; +} + +SDL2DisplayWindow::SDL2DisplayWindow(DisplayWindowHost* windowHost) : WindowHost(windowHost) +{ + CheckInitSDL(); + + int result = SDL_CreateWindowAndRenderer(320, 200, SDL_WINDOW_HIDDEN /*| SDL_WINDOW_ALLOW_HIGHDPI*/, &WindowHandle, &RendererHandle); + if (result != 0) + throw std::runtime_error(std::string("Unable to create SDL window:") + SDL_GetError()); + + WindowList[SDL_GetWindowID(WindowHandle)] = this; +} + +SDL2DisplayWindow::~SDL2DisplayWindow() +{ + WindowList.erase(WindowList.find(SDL_GetWindowID(WindowHandle))); + + if (BackBufferTexture) + { + SDL_DestroyTexture(BackBufferTexture); + BackBufferTexture = nullptr; + } + + SDL_DestroyRenderer(RendererHandle); + SDL_DestroyWindow(WindowHandle); + RendererHandle = nullptr; + WindowHandle = nullptr; +} + +void SDL2DisplayWindow::SetWindowTitle(const std::string& text) +{ + SDL_SetWindowTitle(WindowHandle, text.c_str()); +} + +void SDL2DisplayWindow::SetWindowFrame(const Rect& box) +{ + // SDL2 doesn't really seem to have an API for this. + // The docs aren't clear what you're setting when calling SDL_SetWindowSize. + SetClientFrame(box); +} + +void SDL2DisplayWindow::SetClientFrame(const Rect& box) +{ + // Is there a way to set both in one call? + + double uiscale = GetDpiScale(); + int x = (int)std::round(box.x * uiscale); + int y = (int)std::round(box.y * uiscale); + int w = (int)std::round(box.width * uiscale); + int h = (int)std::round(box.height * uiscale); + + SDL_SetWindowPosition(WindowHandle, x, y); + SDL_SetWindowSize(WindowHandle, w, h); +} + +void SDL2DisplayWindow::Show() +{ + SDL_ShowWindow(WindowHandle); +} + +void SDL2DisplayWindow::ShowFullscreen() +{ + SDL_SetWindowFullscreen(WindowHandle, SDL_WINDOW_FULLSCREEN_DESKTOP); +} + +void SDL2DisplayWindow::ShowMaximized() +{ + SDL_ShowWindow(WindowHandle); + SDL_MaximizeWindow(WindowHandle); +} + +void SDL2DisplayWindow::ShowMinimized() +{ + SDL_ShowWindow(WindowHandle); + SDL_MinimizeWindow(WindowHandle); +} + +void SDL2DisplayWindow::ShowNormal() +{ + SDL_ShowWindow(WindowHandle); + SDL_SetWindowFullscreen(WindowHandle, 0); +} + +void SDL2DisplayWindow::Hide() +{ + SDL_HideWindow(WindowHandle); +} + +void SDL2DisplayWindow::Activate() +{ + SDL_RaiseWindow(WindowHandle); +} + +void SDL2DisplayWindow::ShowCursor(bool enable) +{ + SDL_ShowCursor(enable); +} + +void SDL2DisplayWindow::LockCursor() +{ + SDL_SetWindowGrab(WindowHandle, SDL_TRUE); + SDL_ShowCursor(0); +} + +void SDL2DisplayWindow::UnlockCursor() +{ + SDL_SetWindowGrab(WindowHandle, SDL_FALSE); + SDL_ShowCursor(1); +} + +void SDL2DisplayWindow::CaptureMouse() +{ +} + +void SDL2DisplayWindow::ReleaseMouseCapture() +{ +} + +void SDL2DisplayWindow::SetCursor(StandardCursor cursor) +{ +} + +void SDL2DisplayWindow::Update() +{ + SDL_Event event = {}; + event.type = PaintEventNumber; + event.user.windowID = SDL_GetWindowID(WindowHandle); + SDL_PushEvent(&event); +} + +bool SDL2DisplayWindow::GetKeyState(EInputKey key) +{ + int numkeys = 0; + const Uint8* state = SDL_GetKeyboardState(&numkeys); + if (!state) return false; + + SDL_Scancode index = InputKeyToScancode(key); + return (index < numkeys) ? state[index] != 0 : false; +} + +Rect SDL2DisplayWindow::GetWindowFrame() const +{ + int x = 0; + int y = 0; + int w = 0; + int h = 0; + double uiscale = GetDpiScale(); + SDL_GetWindowPosition(WindowHandle, &x, &y); + SDL_GetWindowSize(WindowHandle, &w, &h); + return Rect::xywh(x / uiscale, y / uiscale, w / uiscale, h / uiscale); +} + +Size SDL2DisplayWindow::GetClientSize() const +{ + int w = 0; + int h = 0; + double uiscale = GetDpiScale(); + SDL_GetWindowSize(WindowHandle, &w, &h); + return Size(w / uiscale, h / uiscale); +} + +int SDL2DisplayWindow::GetPixelWidth() const +{ + int w = 0; + int h = 0; + int result = SDL_GetRendererOutputSize(RendererHandle, &w, &h); + return w; +} + +int SDL2DisplayWindow::GetPixelHeight() const +{ + int w = 0; + int h = 0; + int result = SDL_GetRendererOutputSize(RendererHandle, &w, &h); + return h; +} + +double SDL2DisplayWindow::GetDpiScale() const +{ + // SDL2 doesn't really support this properly. SDL_GetDisplayDPI returns the wrong information according to the docs. + return 1.0; +} + +void SDL2DisplayWindow::PresentBitmap(int width, int height, const uint32_t* pixels) +{ + if (!BackBufferTexture || BackBufferWidth != width || BackBufferHeight != height) + { + if (BackBufferTexture) + { + SDL_DestroyTexture(BackBufferTexture); + BackBufferTexture = nullptr; + } + + BackBufferTexture = SDL_CreateTexture(RendererHandle, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, width, height); + if (!BackBufferTexture) + return; + + BackBufferWidth = width; + BackBufferHeight = height; + } + + int destpitch = 0; + void* dest = nullptr; + int result = SDL_LockTexture(BackBufferTexture, nullptr, &dest, &destpitch); + if (result != 0) return; + for (int y = 0; y < height; y++) + { + const void* sline = pixels + y * width; + void* dline = (uint8_t*)dest + y * destpitch; + memcpy(dline, sline, width << 2); + } + SDL_UnlockTexture(BackBufferTexture); + + SDL_RenderCopy(RendererHandle, BackBufferTexture, nullptr, nullptr); + SDL_RenderPresent(RendererHandle); +} + +void SDL2DisplayWindow::SetBorderColor(uint32_t bgra8) +{ + // SDL doesn't have this +} + +void SDL2DisplayWindow::SetCaptionColor(uint32_t bgra8) +{ + // SDL doesn't have this +} + +void SDL2DisplayWindow::SetCaptionTextColor(uint32_t bgra8) +{ + // SDL doesn't have this +} + +std::string SDL2DisplayWindow::GetClipboardText() +{ + char* buffer = SDL_GetClipboardText(); + if (!buffer) + return {}; + std::string text = buffer; + SDL_free(buffer); + return text; +} + +void SDL2DisplayWindow::SetClipboardText(const std::string& text) +{ + SDL_SetClipboardText(text.c_str()); +} + +void SDL2DisplayWindow::ProcessEvents() +{ + CheckInitSDL(); + + SDL_Event event; + while (SDL_PollEvent(&event) != 0) + { + DispatchEvent(event); + } +} + +void SDL2DisplayWindow::RunLoop() +{ + CheckInitSDL(); + + while (!ExitRunLoop) + { + SDL_Event event; + int result = SDL_WaitEvent(&event); + if (result == 0) + throw std::runtime_error(std::string("SDL_WaitEvent failed:") + SDL_GetError()); + DispatchEvent(event); + } +} + +void SDL2DisplayWindow::ExitLoop() +{ + CheckInitSDL(); + + ExitRunLoop = true; +} + +Size SDL2DisplayWindow::GetScreenSize() +{ + CheckInitSDL(); + + SDL_Rect rect = {}; + int result = SDL_GetDisplayBounds(0, &rect); + if (result != 0) + throw std::runtime_error(std::string("Unable to get screen size:") + SDL_GetError()); + + double uiscale = 1.0; // SDL2 doesn't really support this properly. SDL_GetDisplayDPI returns the wrong information according to the docs. + return Size(rect.w / uiscale, rect.h / uiscale); +} + +void* SDL2DisplayWindow::StartTimer(int timeoutMilliseconds, std::function onTimer) +{ + CheckInitSDL(); + + // To do: implement timers + + return nullptr; +} + +void SDL2DisplayWindow::StopTimer(void* timerID) +{ + CheckInitSDL(); + + // To do: implement timers +} + +SDL2DisplayWindow* SDL2DisplayWindow::FindEventWindow(const SDL_Event& event) +{ + int windowID; + switch (event.type) + { + case SDL_WINDOWEVENT: windowID = event.window.windowID; break; + case SDL_TEXTINPUT: windowID = event.text.windowID; break; + case SDL_KEYUP: windowID = event.key.windowID; break; + case SDL_KEYDOWN: windowID = event.key.windowID; break; + case SDL_MOUSEBUTTONUP: windowID = event.button.windowID; break; + case SDL_MOUSEBUTTONDOWN: windowID = event.button.windowID; break; + case SDL_MOUSEWHEEL: windowID = event.wheel.windowID; break; + case SDL_MOUSEMOTION: windowID = event.motion.windowID; break; + default: + if (event.type == PaintEventNumber) windowID = event.user.windowID; + else return nullptr; + } + + auto it = WindowList.find(windowID); + return it != WindowList.end() ? it->second : nullptr; +} + +void SDL2DisplayWindow::DispatchEvent(const SDL_Event& event) +{ + SDL2DisplayWindow* window = FindEventWindow(event); + if (!window) return; + + switch (event.type) + { + case SDL_WINDOWEVENT: window->OnWindowEvent(event.window); break; + case SDL_TEXTINPUT: window->OnTextInput(event.text); break; + case SDL_KEYUP: window->OnKeyUp(event.key); break; + case SDL_KEYDOWN: window->OnKeyDown(event.key); break; + case SDL_MOUSEBUTTONUP: window->OnMouseButtonUp(event.button); break; + case SDL_MOUSEBUTTONDOWN: window->OnMouseButtonDown(event.button); break; + case SDL_MOUSEWHEEL: window->OnMouseWheel(event.wheel); break; + case SDL_MOUSEMOTION: window->OnMouseMotion(event.motion); break; + default: + if (event.type == PaintEventNumber) window->OnPaintEvent(); + } +} + +void SDL2DisplayWindow::OnWindowEvent(const SDL_WindowEvent& event) +{ + switch (event.event) + { + case SDL_WINDOWEVENT_CLOSE: WindowHost->OnWindowClose(); break; + case SDL_WINDOWEVENT_MOVED: WindowHost->OnWindowGeometryChanged(); break; + case SDL_WINDOWEVENT_RESIZED: WindowHost->OnWindowGeometryChanged(); break; + case SDL_WINDOWEVENT_SHOWN: WindowHost->OnWindowPaint(); break; + case SDL_WINDOWEVENT_EXPOSED: WindowHost->OnWindowPaint(); break; + case SDL_WINDOWEVENT_FOCUS_GAINED: WindowHost->OnWindowActivated(); break; + case SDL_WINDOWEVENT_FOCUS_LOST: WindowHost->OnWindowDeactivated(); break; + } +} + +void SDL2DisplayWindow::OnTextInput(const SDL_TextInputEvent& event) +{ + WindowHost->OnWindowKeyChar(event.text); +} + +void SDL2DisplayWindow::OnKeyUp(const SDL_KeyboardEvent& event) +{ + WindowHost->OnWindowKeyUp(ScancodeToInputKey(event.keysym.scancode)); +} + +void SDL2DisplayWindow::OnKeyDown(const SDL_KeyboardEvent& event) +{ + WindowHost->OnWindowKeyDown(ScancodeToInputKey(event.keysym.scancode)); +} + +EInputKey SDL2DisplayWindow::GetMouseButtonKey(const SDL_MouseButtonEvent& event) +{ + switch (event.button) + { + case SDL_BUTTON_LEFT: return IK_LeftMouse; + case SDL_BUTTON_MIDDLE: return IK_MiddleMouse; + case SDL_BUTTON_RIGHT: return IK_RightMouse; + // case SDL_BUTTON_X1: return IK_XButton1; + // case SDL_BUTTON_X2: return IK_XButton2; + default: return IK_None; + } +} + +void SDL2DisplayWindow::OnMouseButtonUp(const SDL_MouseButtonEvent& event) +{ + EInputKey key = GetMouseButtonKey(event); + if (key != IK_None) + { + WindowHost->OnWindowMouseUp(GetMousePos(event), key); + } +} + +void SDL2DisplayWindow::OnMouseButtonDown(const SDL_MouseButtonEvent& event) +{ + EInputKey key = GetMouseButtonKey(event); + if (key != IK_None) + { + WindowHost->OnWindowMouseDown(GetMousePos(event), key); + } +} + +void SDL2DisplayWindow::OnMouseWheel(const SDL_MouseWheelEvent& event) +{ + EInputKey key = (event.y > 0) ? IK_MouseWheelUp : (event.y < 0) ? IK_MouseWheelDown : IK_None; + if (key != IK_None) + { + WindowHost->OnWindowMouseWheel(GetMousePos(event), key); + } +} + +void SDL2DisplayWindow::OnMouseMotion(const SDL_MouseMotionEvent& event) +{ + WindowHost->OnWindowMouseMove(GetMousePos(event)); +} + +void SDL2DisplayWindow::OnPaintEvent() +{ + WindowHost->OnWindowPaint(); +} + +EInputKey SDL2DisplayWindow::ScancodeToInputKey(SDL_Scancode keycode) +{ + switch (keycode) + { + case SDL_SCANCODE_BACKSPACE: return IK_Backspace; + case SDL_SCANCODE_TAB: return IK_Tab; + case SDL_SCANCODE_CLEAR: return IK_OEMClear; + case SDL_SCANCODE_RETURN: return IK_Enter; + case SDL_SCANCODE_MENU: return IK_Alt; + case SDL_SCANCODE_PAUSE: return IK_Pause; + case SDL_SCANCODE_ESCAPE: return IK_Escape; + case SDL_SCANCODE_SPACE: return IK_Space; + case SDL_SCANCODE_END: return IK_End; + case SDL_SCANCODE_HOME: return IK_Home; + case SDL_SCANCODE_LEFT: return IK_Left; + case SDL_SCANCODE_UP: return IK_Up; + case SDL_SCANCODE_RIGHT: return IK_Right; + case SDL_SCANCODE_DOWN: return IK_Down; + case SDL_SCANCODE_SELECT: return IK_Select; + case SDL_SCANCODE_PRINTSCREEN: return IK_Print; + case SDL_SCANCODE_EXECUTE: return IK_Execute; + case SDL_SCANCODE_INSERT: return IK_Insert; + case SDL_SCANCODE_DELETE: return IK_Delete; + case SDL_SCANCODE_HELP: return IK_Help; + case SDL_SCANCODE_0: return IK_0; + case SDL_SCANCODE_1: return IK_1; + case SDL_SCANCODE_2: return IK_2; + case SDL_SCANCODE_3: return IK_3; + case SDL_SCANCODE_4: return IK_4; + case SDL_SCANCODE_5: return IK_5; + case SDL_SCANCODE_6: return IK_6; + case SDL_SCANCODE_7: return IK_7; + case SDL_SCANCODE_8: return IK_8; + case SDL_SCANCODE_9: return IK_9; + case SDL_SCANCODE_A: return IK_A; + case SDL_SCANCODE_B: return IK_B; + case SDL_SCANCODE_C: return IK_C; + case SDL_SCANCODE_D: return IK_D; + case SDL_SCANCODE_E: return IK_E; + case SDL_SCANCODE_F: return IK_F; + case SDL_SCANCODE_G: return IK_G; + case SDL_SCANCODE_H: return IK_H; + case SDL_SCANCODE_I: return IK_I; + case SDL_SCANCODE_J: return IK_J; + case SDL_SCANCODE_K: return IK_K; + case SDL_SCANCODE_L: return IK_L; + case SDL_SCANCODE_M: return IK_M; + case SDL_SCANCODE_N: return IK_N; + case SDL_SCANCODE_O: return IK_O; + case SDL_SCANCODE_P: return IK_P; + case SDL_SCANCODE_Q: return IK_Q; + case SDL_SCANCODE_R: return IK_R; + case SDL_SCANCODE_S: return IK_S; + case SDL_SCANCODE_T: return IK_T; + case SDL_SCANCODE_U: return IK_U; + case SDL_SCANCODE_V: return IK_V; + case SDL_SCANCODE_W: return IK_W; + case SDL_SCANCODE_X: return IK_X; + case SDL_SCANCODE_Y: return IK_Y; + case SDL_SCANCODE_Z: return IK_Z; + case SDL_SCANCODE_KP_0: return IK_NumPad0; + case SDL_SCANCODE_KP_1: return IK_NumPad1; + case SDL_SCANCODE_KP_2: return IK_NumPad2; + case SDL_SCANCODE_KP_3: return IK_NumPad3; + case SDL_SCANCODE_KP_4: return IK_NumPad4; + case SDL_SCANCODE_KP_5: return IK_NumPad5; + case SDL_SCANCODE_KP_6: return IK_NumPad6; + case SDL_SCANCODE_KP_7: return IK_NumPad7; + case SDL_SCANCODE_KP_8: return IK_NumPad8; + case SDL_SCANCODE_KP_9: return IK_NumPad9; + // case SDL_SCANCODE_KP_ENTER: return IK_NumPadEnter; + // case SDL_SCANCODE_KP_MULTIPLY: return IK_Multiply; + // case SDL_SCANCODE_KP_PLUS: return IK_Add; + case SDL_SCANCODE_SEPARATOR: return IK_Separator; + // case SDL_SCANCODE_KP_MINUS: return IK_Subtract; + case SDL_SCANCODE_KP_PERIOD: return IK_NumPadPeriod; + // case SDL_SCANCODE_KP_DIVIDE: return IK_Divide; + case SDL_SCANCODE_F1: return IK_F1; + case SDL_SCANCODE_F2: return IK_F2; + case SDL_SCANCODE_F3: return IK_F3; + case SDL_SCANCODE_F4: return IK_F4; + case SDL_SCANCODE_F5: return IK_F5; + case SDL_SCANCODE_F6: return IK_F6; + case SDL_SCANCODE_F7: return IK_F7; + case SDL_SCANCODE_F8: return IK_F8; + case SDL_SCANCODE_F9: return IK_F9; + case SDL_SCANCODE_F10: return IK_F10; + case SDL_SCANCODE_F11: return IK_F11; + case SDL_SCANCODE_F12: return IK_F12; + case SDL_SCANCODE_F13: return IK_F13; + case SDL_SCANCODE_F14: return IK_F14; + case SDL_SCANCODE_F15: return IK_F15; + case SDL_SCANCODE_F16: return IK_F16; + case SDL_SCANCODE_F17: return IK_F17; + case SDL_SCANCODE_F18: return IK_F18; + case SDL_SCANCODE_F19: return IK_F19; + case SDL_SCANCODE_F20: return IK_F20; + case SDL_SCANCODE_F21: return IK_F21; + case SDL_SCANCODE_F22: return IK_F22; + case SDL_SCANCODE_F23: return IK_F23; + case SDL_SCANCODE_F24: return IK_F24; + case SDL_SCANCODE_NUMLOCKCLEAR: return IK_NumLock; + case SDL_SCANCODE_SCROLLLOCK: return IK_ScrollLock; + case SDL_SCANCODE_LSHIFT: return IK_LShift; + case SDL_SCANCODE_RSHIFT: return IK_RShift; + case SDL_SCANCODE_LCTRL: return IK_LControl; + case SDL_SCANCODE_RCTRL: return IK_RControl; + case SDL_SCANCODE_GRAVE: return IK_Tilde; + default: return IK_None; + } +} + +SDL_Scancode SDL2DisplayWindow::InputKeyToScancode(EInputKey inputkey) +{ + switch (inputkey) + { + case IK_Backspace: return SDL_SCANCODE_BACKSPACE; + case IK_Tab: return SDL_SCANCODE_TAB; + case IK_OEMClear: return SDL_SCANCODE_CLEAR; + case IK_Enter: return SDL_SCANCODE_RETURN; + case IK_Alt: return SDL_SCANCODE_MENU; + case IK_Pause: return SDL_SCANCODE_PAUSE; + case IK_Escape: return SDL_SCANCODE_ESCAPE; + case IK_Space: return SDL_SCANCODE_SPACE; + case IK_End: return SDL_SCANCODE_END; + case IK_Home: return SDL_SCANCODE_HOME; + case IK_Left: return SDL_SCANCODE_LEFT; + case IK_Up: return SDL_SCANCODE_UP; + case IK_Right: return SDL_SCANCODE_RIGHT; + case IK_Down: return SDL_SCANCODE_DOWN; + case IK_Select: return SDL_SCANCODE_SELECT; + case IK_Print: return SDL_SCANCODE_PRINTSCREEN; + case IK_Execute: return SDL_SCANCODE_EXECUTE; + case IK_Insert: return SDL_SCANCODE_INSERT; + case IK_Delete: return SDL_SCANCODE_DELETE; + case IK_Help: return SDL_SCANCODE_HELP; + case IK_0: return SDL_SCANCODE_0; + case IK_1: return SDL_SCANCODE_1; + case IK_2: return SDL_SCANCODE_2; + case IK_3: return SDL_SCANCODE_3; + case IK_4: return SDL_SCANCODE_4; + case IK_5: return SDL_SCANCODE_5; + case IK_6: return SDL_SCANCODE_6; + case IK_7: return SDL_SCANCODE_7; + case IK_8: return SDL_SCANCODE_8; + case IK_9: return SDL_SCANCODE_9; + case IK_A: return SDL_SCANCODE_A; + case IK_B: return SDL_SCANCODE_B; + case IK_C: return SDL_SCANCODE_C; + case IK_D: return SDL_SCANCODE_D; + case IK_E: return SDL_SCANCODE_E; + case IK_F: return SDL_SCANCODE_F; + case IK_G: return SDL_SCANCODE_G; + case IK_H: return SDL_SCANCODE_H; + case IK_I: return SDL_SCANCODE_I; + case IK_J: return SDL_SCANCODE_J; + case IK_K: return SDL_SCANCODE_K; + case IK_L: return SDL_SCANCODE_L; + case IK_M: return SDL_SCANCODE_M; + case IK_N: return SDL_SCANCODE_N; + case IK_O: return SDL_SCANCODE_O; + case IK_P: return SDL_SCANCODE_P; + case IK_Q: return SDL_SCANCODE_Q; + case IK_R: return SDL_SCANCODE_R; + case IK_S: return SDL_SCANCODE_S; + case IK_T: return SDL_SCANCODE_T; + case IK_U: return SDL_SCANCODE_U; + case IK_V: return SDL_SCANCODE_V; + case IK_W: return SDL_SCANCODE_W; + case IK_X: return SDL_SCANCODE_X; + case IK_Y: return SDL_SCANCODE_Y; + case IK_Z: return SDL_SCANCODE_Z; + case IK_NumPad0: return SDL_SCANCODE_KP_0; + case IK_NumPad1: return SDL_SCANCODE_KP_1; + case IK_NumPad2: return SDL_SCANCODE_KP_2; + case IK_NumPad3: return SDL_SCANCODE_KP_3; + case IK_NumPad4: return SDL_SCANCODE_KP_4; + case IK_NumPad5: return SDL_SCANCODE_KP_5; + case IK_NumPad6: return SDL_SCANCODE_KP_6; + case IK_NumPad7: return SDL_SCANCODE_KP_7; + case IK_NumPad8: return SDL_SCANCODE_KP_8; + case IK_NumPad9: return SDL_SCANCODE_KP_9; + // case IK_NumPadEnter: return SDL_SCANCODE_KP_ENTER; + // case IK_Multiply return SDL_SCANCODE_KP_MULTIPLY:; + // case IK_Add: return SDL_SCANCODE_KP_PLUS; + case IK_Separator: return SDL_SCANCODE_SEPARATOR; + // case IK_Subtract: return SDL_SCANCODE_KP_MINUS; + case IK_NumPadPeriod: return SDL_SCANCODE_KP_PERIOD; + // case IK_Divide: return SDL_SCANCODE_KP_DIVIDE; + case IK_F1: return SDL_SCANCODE_F1; + case IK_F2: return SDL_SCANCODE_F2; + case IK_F3: return SDL_SCANCODE_F3; + case IK_F4: return SDL_SCANCODE_F4; + case IK_F5: return SDL_SCANCODE_F5; + case IK_F6: return SDL_SCANCODE_F6; + case IK_F7: return SDL_SCANCODE_F7; + case IK_F8: return SDL_SCANCODE_F8; + case IK_F9: return SDL_SCANCODE_F9; + case IK_F10: return SDL_SCANCODE_F10; + case IK_F11: return SDL_SCANCODE_F11; + case IK_F12: return SDL_SCANCODE_F12; + case IK_F13: return SDL_SCANCODE_F13; + case IK_F14: return SDL_SCANCODE_F14; + case IK_F15: return SDL_SCANCODE_F15; + case IK_F16: return SDL_SCANCODE_F16; + case IK_F17: return SDL_SCANCODE_F17; + case IK_F18: return SDL_SCANCODE_F18; + case IK_F19: return SDL_SCANCODE_F19; + case IK_F20: return SDL_SCANCODE_F20; + case IK_F21: return SDL_SCANCODE_F21; + case IK_F22: return SDL_SCANCODE_F22; + case IK_F23: return SDL_SCANCODE_F23; + case IK_F24: return SDL_SCANCODE_F24; + case IK_NumLock: return SDL_SCANCODE_NUMLOCKCLEAR; + case IK_ScrollLock: return SDL_SCANCODE_SCROLLLOCK; + case IK_LShift: return SDL_SCANCODE_LSHIFT; + case IK_RShift: return SDL_SCANCODE_RSHIFT; + case IK_LControl: return SDL_SCANCODE_LCTRL; + case IK_RControl: return SDL_SCANCODE_RCTRL; + case IK_Tilde: return SDL_SCANCODE_GRAVE; + default: return (SDL_Scancode)0; + } +} diff --git a/src/window/sdl2/sdl2displaywindow.h b/src/window/sdl2/sdl2displaywindow.h new file mode 100644 index 0000000..fa0e825 --- /dev/null +++ b/src/window/sdl2/sdl2displaywindow.h @@ -0,0 +1,91 @@ +#pragma once + +#include +#include +#include +#include + +class SDL2DisplayWindow : public DisplayWindow +{ +public: + SDL2DisplayWindow(DisplayWindowHost* windowHost); + ~SDL2DisplayWindow(); + + void SetWindowTitle(const std::string& text) override; + void SetWindowFrame(const Rect& box) override; + void SetClientFrame(const Rect& box) override; + void Show() override; + void ShowFullscreen() override; + void ShowMaximized() override; + void ShowMinimized() override; + void ShowNormal() override; + void Hide() override; + void Activate() override; + void ShowCursor(bool enable) override; + void LockCursor() override; + void UnlockCursor() override; + void CaptureMouse() override; + void ReleaseMouseCapture() override; + void Update() override; + bool GetKeyState(EInputKey key) override; + void SetCursor(StandardCursor cursor) override; + + Rect GetWindowFrame() const override; + Size GetClientSize() const override; + int GetPixelWidth() const override; + int GetPixelHeight() const override; + double GetDpiScale() const override; + + void PresentBitmap(int width, int height, const uint32_t* pixels) override; + + void SetBorderColor(uint32_t bgra8) override; + void SetCaptionColor(uint32_t bgra8) override; + void SetCaptionTextColor(uint32_t bgra8) override; + + std::string GetClipboardText() override; + void SetClipboardText(const std::string& text) override; + + static void DispatchEvent(const SDL_Event& event); + static SDL2DisplayWindow* FindEventWindow(const SDL_Event& event); + + void OnWindowEvent(const SDL_WindowEvent& event); + void OnTextInput(const SDL_TextInputEvent& event); + void OnKeyUp(const SDL_KeyboardEvent& event); + void OnKeyDown(const SDL_KeyboardEvent& event); + void OnMouseButtonUp(const SDL_MouseButtonEvent& event); + void OnMouseButtonDown(const SDL_MouseButtonEvent& event); + void OnMouseWheel(const SDL_MouseWheelEvent& event); + void OnMouseMotion(const SDL_MouseMotionEvent& event); + void OnPaintEvent(); + + EInputKey GetMouseButtonKey(const SDL_MouseButtonEvent& event); + + static EInputKey ScancodeToInputKey(SDL_Scancode keycode); + static SDL_Scancode InputKeyToScancode(EInputKey inputkey); + + template + Point GetMousePos(const T& event) + { + double uiscale = GetDpiScale(); + return Point(event.x / uiscale, event.y / uiscale); + } + + static void ProcessEvents(); + static void RunLoop(); + static void ExitLoop(); + static Size GetScreenSize(); + + static void* StartTimer(int timeoutMilliseconds, std::function onTimer); + static void StopTimer(void* timerID); + + DisplayWindowHost* WindowHost = nullptr; + SDL_Window* WindowHandle = nullptr; + SDL_Renderer* RendererHandle = nullptr; + SDL_Texture* BackBufferTexture = nullptr; + int BackBufferWidth = 0; + int BackBufferHeight = 0; + + static bool ExitRunLoop; + static Uint32 PaintEventNumber; + static std::unordered_map WindowList; +}; diff --git a/src/window/win32/win32window.cpp b/src/window/win32/win32window.cpp index 58a2c92..f279bcf 100644 --- a/src/window/win32/win32window.cpp +++ b/src/window/win32/win32window.cpp @@ -64,7 +64,7 @@ Win32Window::Win32Window(DisplayWindowHost* windowHost) : WindowHost(windowHost) WNDCLASSEX classdesc = {}; classdesc.cbSize = sizeof(WNDCLASSEX); classdesc.hInstance = GetModuleHandle(0); - classdesc.style = CS_VREDRAW | CS_HREDRAW; + classdesc.style = CS_VREDRAW | CS_HREDRAW | CS_DBLCLKS; classdesc.lpszClassName = L"ZWidgetWindow"; classdesc.lpfnWndProc = &Win32Window::WndProc; RegisterClassEx(&classdesc); @@ -209,6 +209,16 @@ void Win32Window::UnlockCursor() } } +void Win32Window::CaptureMouse() +{ + SetCapture(WindowHandle); +} + +void Win32Window::ReleaseMouseCapture() +{ + ReleaseCapture(); +} + void Win32Window::Update() { InvalidateRect(WindowHandle, nullptr, FALSE); @@ -219,6 +229,15 @@ bool Win32Window::GetKeyState(EInputKey key) return ::GetKeyState((int)key) & 0x8000; // High bit (0x8000) means key is down, Low bit (0x0001) means key is sticky on (like Caps Lock, Num Lock, etc.) } +void Win32Window::SetCursor(StandardCursor cursor) +{ + if (cursor != CurrentCursor) + { + CurrentCursor = cursor; + UpdateCursor(); + } +} + Rect Win32Window::GetWindowFrame() const { RECT box = {}; @@ -411,7 +430,7 @@ LRESULT Win32Window::OnWindowMessage(UINT msg, WPARAM wparam, LPARAM lparam) } else { - SetCursor((HCURSOR)LoadImage(0, IDC_ARROW, IMAGE_CURSOR, LR_DEFAULTSIZE, LR_DEFAULTSIZE, LR_SHARED)); + UpdateCursor(); } WindowHost->OnWindowMouseMove(GetLParamPos(lparam)); @@ -420,6 +439,10 @@ LRESULT Win32Window::OnWindowMessage(UINT msg, WPARAM wparam, LPARAM lparam) { WindowHost->OnWindowMouseDown(GetLParamPos(lparam), IK_LeftMouse); } + else if (msg == WM_LBUTTONDBLCLK) + { + WindowHost->OnWindowMouseDoubleclick(GetLParamPos(lparam), IK_LeftMouse); + } else if (msg == WM_LBUTTONUP) { WindowHost->OnWindowMouseUp(GetLParamPos(lparam), IK_LeftMouse); @@ -428,6 +451,10 @@ LRESULT Win32Window::OnWindowMessage(UINT msg, WPARAM wparam, LPARAM lparam) { WindowHost->OnWindowMouseDown(GetLParamPos(lparam), IK_MiddleMouse); } + else if (msg == WM_MBUTTONDBLCLK) + { + WindowHost->OnWindowMouseDoubleclick(GetLParamPos(lparam), IK_MiddleMouse); + } else if (msg == WM_MBUTTONUP) { WindowHost->OnWindowMouseUp(GetLParamPos(lparam), IK_MiddleMouse); @@ -436,6 +463,10 @@ LRESULT Win32Window::OnWindowMessage(UINT msg, WPARAM wparam, LPARAM lparam) { WindowHost->OnWindowMouseDown(GetLParamPos(lparam), IK_RightMouse); } + else if (msg == WM_RBUTTONDBLCLK) + { + WindowHost->OnWindowMouseDoubleclick(GetLParamPos(lparam), IK_RightMouse); + } else if (msg == WM_RBUTTONUP) { WindowHost->OnWindowMouseUp(GetLParamPos(lparam), IK_RightMouse); @@ -443,7 +474,15 @@ LRESULT Win32Window::OnWindowMessage(UINT msg, WPARAM wparam, LPARAM lparam) else if (msg == WM_MOUSEWHEEL) { double delta = GET_WHEEL_DELTA_WPARAM(wparam) / (double)WHEEL_DELTA; - WindowHost->OnWindowMouseWheel(GetLParamPos(lparam), delta < 0.0 ? IK_MouseWheelDown : IK_MouseWheelUp); + + // Note: WM_MOUSEWHEEL uses screen coordinates. GetLParamPos assumes client coordinates. + double dpiscale = GetDpiScale(); + POINT pos; + pos.x = GET_X_LPARAM(lparam); + pos.y = GET_Y_LPARAM(lparam); + ScreenToClient(WindowHandle, &pos); + + WindowHost->OnWindowMouseWheel(Point(pos.x / dpiscale, pos.y / dpiscale), delta < 0.0 ? IK_MouseWheelDown : IK_MouseWheelUp); } else if (msg == WM_CHAR) { @@ -491,6 +530,30 @@ LRESULT Win32Window::OnWindowMessage(UINT msg, WPARAM wparam, LPARAM lparam) return DefWindowProc(WindowHandle, msg, wparam, lparam); } +void Win32Window::UpdateCursor() +{ + LPCWSTR cursor = IDC_ARROW; + switch (CurrentCursor) + { + case StandardCursor::arrow: cursor = IDC_ARROW; break; + case StandardCursor::appstarting: cursor = IDC_APPSTARTING; break; + case StandardCursor::cross: cursor = IDC_CROSS; break; + case StandardCursor::hand: cursor = IDC_HAND; break; + case StandardCursor::ibeam: cursor = IDC_IBEAM; break; + case StandardCursor::no: cursor = IDC_NO; break; + case StandardCursor::size_all: cursor = IDC_SIZEALL; break; + case StandardCursor::size_nesw: cursor = IDC_SIZENESW; break; + case StandardCursor::size_ns: cursor = IDC_SIZENS; break; + case StandardCursor::size_nwse: cursor = IDC_SIZENWSE; break; + case StandardCursor::size_we: cursor = IDC_SIZEWE; break; + case StandardCursor::uparrow: cursor = IDC_UPARROW; break; + case StandardCursor::wait: cursor = IDC_WAIT; break; + default: break; + } + + ::SetCursor((HCURSOR)LoadImage(0, cursor, IMAGE_CURSOR, LR_DEFAULTSIZE, LR_DEFAULTSIZE, LR_SHARED)); +} + Point Win32Window::GetLParamPos(LPARAM lparam) const { double dpiscale = GetDpiScale(); diff --git a/src/window/win32/win32window.h b/src/window/win32/win32window.h index 74f56f1..a0f493c 100644 --- a/src/window/win32/win32window.h +++ b/src/window/win32/win32window.h @@ -30,9 +30,14 @@ class Win32Window : public DisplayWindow void ShowCursor(bool enable) override; void LockCursor() override; void UnlockCursor() override; + void CaptureMouse() override; + void ReleaseMouseCapture() override; void Update() override; bool GetKeyState(EInputKey key) override; + void SetCursor(StandardCursor cursor) override; + void UpdateCursor(); + Rect GetWindowFrame() const override; Size GetClientSize() const override; int GetPixelWidth() const override; @@ -76,4 +81,6 @@ class Win32Window : public DisplayWindow POINT MouseLockPos = {}; HDC PaintDC = 0; + + StandardCursor CurrentCursor = StandardCursor::arrow; }; diff --git a/src/window/window.cpp b/src/window/window.cpp index 909a2e3..234be61 100644 --- a/src/window/window.cpp +++ b/src/window/window.cpp @@ -41,7 +41,7 @@ void DisplayWindow::StopTimer(void* timerID) Win32Window::StopTimer(timerID); } -#else +#elif defined(__APPLE__) std::unique_ptr DisplayWindow::Create(DisplayWindowHost* windowHost) { @@ -78,4 +78,43 @@ void DisplayWindow::StopTimer(void* timerID) throw std::runtime_error("DisplayWindow::StopTimer not implemented"); } +#else + +#include "sdl2/sdl2displaywindow.h" + +std::unique_ptr DisplayWindow::Create(DisplayWindowHost* windowHost) +{ + return std::make_unique(windowHost); +} + +void DisplayWindow::ProcessEvents() +{ + SDL2DisplayWindow::ProcessEvents(); +} + +void DisplayWindow::RunLoop() +{ + SDL2DisplayWindow::RunLoop(); +} + +void DisplayWindow::ExitLoop() +{ + SDL2DisplayWindow::ExitLoop(); +} + +Size DisplayWindow::GetScreenSize() +{ + return SDL2DisplayWindow::GetScreenSize(); +} + +void* DisplayWindow::StartTimer(int timeoutMilliseconds, std::function onTimer) +{ + return SDL2DisplayWindow::StartTimer(timeoutMilliseconds, std::move(onTimer)); +} + +void DisplayWindow::StopTimer(void* timerID) +{ + SDL2DisplayWindow::StopTimer(timerID); +} + #endif