From ed2ff2908a27883a3efe5f1976f9830bf2b1a279 Mon Sep 17 00:00:00 2001 From: John Curley Date: Sat, 15 Nov 2025 14:38:12 +1300 Subject: [PATCH 1/3] updated macOS cocoa backend to correctly render instead of sdl2/3 --- CMakeLists.txt | 46 ++-- include/zwidget/widgets/dropdown/dropdown.h | 2 + src/core/canvas.cpp | 5 + src/core/resourcedata_mac.mm | 104 +++++--- src/core/resourcedata_unix.cpp | 38 +-- src/core/truetypefont.cpp | 23 +- src/core/truetypefont.h | 2 + src/core/widget.cpp | 70 ++++- src/widgets/dropdown/dropdown.cpp | 41 ++- src/widgets/listview/listview.cpp | 10 +- src/widgets/tabwidget/tabwidget.cpp | 8 + src/widgets/textedit/textedit.cpp | 50 +++- src/window/cocoa/cocoa_display_backend.mm | 68 +++-- src/window/cocoa/cocoa_display_window.mm | 272 ++++++++++++++------ src/window/cocoa/cocoa_open_file_dialog.h | 3 + src/window/cocoa/cocoa_open_file_dialog.mm | 77 ++++-- src/window/cocoa/cocoa_save_file_dialog.mm | 6 +- src/window/window.cpp | 15 +- 18 files changed, 616 insertions(+), 224 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a4f3db4..64aaa47 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,7 @@ project(zwidget) option(ENABLE_METAL "Enable Metal support for application rendering" ON) option(ENABLE_OPENGL "Enable OpenGL support for application rendering" ON) +option(ENABLE_SDL_ON_MACOS "Enable SDL backends on macOS (native Cocoa is preferred)" OFF) if (UNIX AND NOT APPLE) include(FindPkgConfig) @@ -288,8 +289,17 @@ elseif(APPLE) message(FATAL_ERROR "Cocoa framework not found") endif() + find_library(COREVIDEO_FRAMEWORK CoreVideo) + if (NOT COREVIDEO_FRAMEWORK) + message(FATAL_ERROR "CoreVideo framework not found") + endif() + if(ENABLE_METAL) find_library(METAL_FRAMEWORK Metal) + find_library(QUARTZCORE_FRAMEWORK QuartzCore) + if (NOT QUARTZCORE_FRAMEWORK) + message(FATAL_ERROR "QuartzCore framework not found") + endif() endif() if(ENABLE_OPENGL) @@ -298,7 +308,7 @@ elseif(APPLE) message(FATAL_ERROR "OpenGL framework not found") endif() endif() - + find_library(UNIFORMTYPEIDENTIFIERS_FRAMEWORK UniformTypeIdentifiers) if (NOT UNIFORMTYPEIDENTIFIERS_FRAMEWORK) message(FATAL_ERROR "UniformTypeIdentifiers framework not found") @@ -328,19 +338,20 @@ else() set(ZWIDGET_DEFINES ${ZWIDGET_DEFINES} -DUSE_WAYLAND) endif() - set(ZWIDGET_INCLUDE_DIRS ${ZWIDGET_INCLUDE_DIRS} ${GLIB_INCLUDE_DIRS}) - set(ZWIDGET_LIBS ${ZWIDGET_LIBS} ${GLIB_LDFLAGS} -lgio-2.0) + # Note: GTK/GIO no longer required - using fontconfig directly + # set(ZWIDGET_INCLUDE_DIRS ${ZWIDGET_INCLUDE_DIRS} ${GLIB_INCLUDE_DIRS}) + # set(ZWIDGET_LIBS ${ZWIDGET_LIBS} ${GLIB_LDFLAGS} -lgio-2.0) set(ZWIDGET_INCLUDE_DIRS ${ZWIDGET_INCLUDE_DIRS} ${FONTCONFIG_INCLUDE_DIRS}) set(ZWIDGET_LIBS ${ZWIDGET_LIBS} ${FONTCONFIG_LDFLAGS}) endif() -if(SDL3_FOUND AND NOT WIN32) +if(SDL3_FOUND AND NOT WIN32 AND (NOT APPLE OR ENABLE_SDL_ON_MACOS)) set(ZWIDGET_SOURCES ${ZWIDGET_SOURCES} ${ZWIDGET_SDL3_SOURCES}) set(ZWIDGET_DEFINES ${ZWIDGET_DEFINES} -DUSE_SDL3) endif() -if(SDL2_FOUND AND NOT SDL3_FOUND AND NOT WIN32) +if(SDL2_FOUND AND NOT SDL3_FOUND AND NOT WIN32 AND (NOT APPLE OR ENABLE_SDL_ON_MACOS)) set(ZWIDGET_SOURCES ${ZWIDGET_SOURCES} ${ZWIDGET_SDL2_SOURCES}) set(ZWIDGET_DEFINES ${ZWIDGET_DEFINES} -DUSE_SDL2) endif() @@ -357,35 +368,35 @@ target_compile_definitions(zwidget PRIVATE ${ZWIDGET_DEFINES}) target_include_directories(zwidget PRIVATE ${ZWIDGET_INCLUDE_DIRS}) target_link_options(zwidget PRIVATE ${ZWIDGET_LINK_OPTIONS}) if(APPLE) - target_link_libraries(zwidget PRIVATE ${ZWIDGET_LIBS} ${COCOA_FRAMEWORK} ${UNIFORMTYPEIDENTIFIERS_FRAMEWORK}) + target_link_libraries(zwidget PRIVATE ${ZWIDGET_LIBS} ${COCOA_FRAMEWORK} ${COREVIDEO_FRAMEWORK} ${UNIFORMTYPEIDENTIFIERS_FRAMEWORK} ${QUARTZCORE_FRAMEWORK}) if(ENABLE_OPENGL AND OPENGL_FRAMEWORK) target_link_libraries(zwidget PRIVATE ${OPENGL_FRAMEWORK}) target_compile_definitions(zwidget PRIVATE HAVE_OPENGL) endif() if(ENABLE_METAL AND METAL_FRAMEWORK) - target_link_libraries(zwidget PRIVATE ${METAL_FRAMEWORK}) + target_link_libraries(zwidget PRIVATE ${METAL_FRAMEWORK} ${QUARTZCORE_FRAMEWORK}) target_compile_definitions(zwidget PRIVATE HAVE_METAL) endif() else() target_link_libraries(zwidget PRIVATE ${ZWIDGET_LIBS}) endif() -if(SDL3_FOUND) +if(SDL3_FOUND AND (NOT APPLE OR ENABLE_SDL_ON_MACOS)) target_include_directories(zwidget PRIVATE ${SDL3_INCLUDE_DIRS}) if(TARGET SDL3::SDL3) target_link_libraries(zwidget PRIVATE SDL3::SDL3) - else() # needed for gzdoom compat for now + else() target_link_libraries(zwidget PRIVATE ${SDL3_LIBRARY}) endif() endif() -if(SDL2_FOUND AND NOT SDL3_FOUND) +if(SDL2_FOUND AND NOT SDL3_FOUND AND (NOT APPLE OR ENABLE_SDL_ON_MACOS)) target_include_directories(zwidget PRIVATE ${SDL2_INCLUDE_DIRS}) if(TARGET SDL2::SDL2) target_link_libraries(zwidget PRIVATE SDL2::SDL2) - else() # needed for gzdoom compat for now + else() target_link_libraries(zwidget PRIVATE ${SDL2_LIBRARY}) endif() endif() -set_target_properties(zwidget PROPERTIES CXX_STANDARD 20) +set_target_properties(zwidget PROPERTIES CXX_STANDARD 17) target_compile_options(zwidget PRIVATE ${CXX_WARNING_FLAGS}) if(MSVC) @@ -417,23 +428,26 @@ if(ZWIDGET_BUILD_EXAMPLE) target_compile_definitions(zwidget_example PRIVATE UNICODE _UNICODE) target_link_libraries(zwidget_example PRIVATE gdi32 user32 shell32 comdlg32) elseif(APPLE) - target_link_libraries(zwidget_example PRIVATE "-framework Cocoa") + target_link_libraries(zwidget_example PRIVATE "-framework Cocoa" "-framework CoreVideo") if(ENABLE_OPENGL AND OPENGL_FRAMEWORK) target_link_libraries(zwidget_example PRIVATE "-framework OpenGL") endif() + if(ENABLE_METAL AND METAL_FRAMEWORK) + target_link_libraries(zwidget_example PRIVATE "-framework Metal" "-framework QuartzCore") + endif() else() target_link_libraries(zwidget_example PRIVATE ${ZWIDGET_LIBS}) endif() - if(SDL3_FOUND) + if(SDL3_FOUND AND (NOT APPLE OR ENABLE_SDL_ON_MACOS)) target_link_libraries(zwidget_example PRIVATE SDL3::SDL3) endif() - if(SDL2_FOUND AND NOT SDL3_FOUND) + if(SDL2_FOUND AND NOT SDL3_FOUND AND (NOT APPLE OR ENABLE_SDL_ON_MACOS)) target_link_libraries(zwidget_example PRIVATE SDL2::SDL2) endif() - set_target_properties(zwidget_example PROPERTIES CXX_STANDARD 20) + set_target_properties(zwidget_example PROPERTIES CXX_STANDARD 17) if(MSVC) set_property(TARGET zwidget_example PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") diff --git a/include/zwidget/widgets/dropdown/dropdown.h b/include/zwidget/widgets/dropdown/dropdown.h index 31e9a5d..1c44068 100644 --- a/include/zwidget/widgets/dropdown/dropdown.h +++ b/include/zwidget/widgets/dropdown/dropdown.h @@ -20,6 +20,7 @@ class Dropdown : public Widget { public: Dropdown(Widget* parent); + ~Dropdown(); void AddItem(const std::string& text, int index = -1); bool UpdateItem(const std::string& text, int index); @@ -61,6 +62,7 @@ class Dropdown : public Widget bool dropdownOpen = false; Widget* dropdown = nullptr; DropdownList* listView = nullptr; + Widget* pendingDeleteDropdown = nullptr; int maxDisplayItems = 0; bool dropdownDirection = true; diff --git a/src/core/canvas.cpp b/src/core/canvas.cpp index d37b926..f8f934d 100644 --- a/src/core/canvas.cpp +++ b/src/core/canvas.cpp @@ -248,9 +248,13 @@ void Canvas::pushClip(const Rect& box) y1 = std::min(y1, clip.y + clip.height); if (x0 < x1 && y0 < y1) + { clipStack.push_back(Rect::ltrb(x0, y0, x1, y1)); + } else + { clipStack.push_back(Rect::xywh(0.0, 0.0, 0.0, 0.0)); + } } else { @@ -1093,6 +1097,7 @@ void BitmapCanvas::begin(const Colorf& color) uint32_t b = (int32_t)clamp(color.b * 255.0f, 0.0f, 255.0f); uint32_t a = (int32_t)clamp(color.a * 255.0f, 0.0f, 255.0f); uint32_t bgcolor = (a << 24) | (r << 16) | (g << 8) | b; + pixels.clear(); pixels.resize(width * height, bgcolor); } diff --git a/src/core/resourcedata_mac.mm b/src/core/resourcedata_mac.mm index 36f0b39..6b2aee0 100644 --- a/src/core/resourcedata_mac.mm +++ b/src/core/resourcedata_mac.mm @@ -24,32 +24,49 @@ CTFontRef ctFont = (__bridge CTFontRef)systemFont; CFURLRef fontURL = (CFURLRef)CTFontCopyAttribute(ctFont, kCTFontURLAttribute); - if (!fontURL) - throw std::runtime_error("Failed to get font URL from system font"); - // __bridge_transfer transfers ownership to ARC, so no manual CFRelease is needed - NSString* fontPath = (NSString*)CFURLCopyFileSystemPath(fontURL, kCFURLPOSIXPathStyle); - if (!fontPath) - throw std::runtime_error("Failed to convert font URL to file path"); + std::string fontPath; - // Read the font file data - try - { - fontData.fontdata = ReadAllBytes(std::string([fontPath UTF8String])); - } - catch (const std::exception& e) + if (fontURL) { + CFStringRef cfPath = CFURLCopyFileSystemPath(fontURL, kCFURLPOSIXPathStyle); + if (cfPath) + { + fontPath = std::string([(NSString*)cfPath UTF8String]); + CFRelease(cfPath); + } CFRelease(fontURL); - throw std::runtime_error(std::string("Error reading system font file: ") + e.what()); } - catch (...) + + // Fallback to known macOS font paths if system font URL lookup fails + // This handles newer macOS versions where system fonts might be embedded + if (fontPath.empty()) { - CFRelease(fontURL); - throw; + // Try common macOS system font locations + const char* fallbackPaths[] = { + "/System/Library/Fonts/SFNS.ttf", // San Francisco (newer macOS) + "/System/Library/Fonts/SFNSText.ttf", // San Francisco Text + "/System/Library/Fonts/Helvetica.ttc", // Helvetica + "/System/Library/Fonts/HelveticaNeue.ttc", // Helvetica Neue + "/System/Library/Fonts/LucidaGrande.ttc", // Lucida Grande (older macOS) + "/Library/Fonts/Arial.ttf", // Arial fallback + }; + + for (const char* path : fallbackPaths) + { + std::ifstream test(path); + if (test.good()) + { + fontPath = path; + break; + } + } } - CFRelease(fontURL); - // fontPath is __bridge_transfer, so it's autoreleased(ARC) + if (fontPath.empty()) + throw std::runtime_error("Could not find system font"); + + fontData.fontdata = ReadAllBytes(fontPath); } return { fontData }; } @@ -65,32 +82,47 @@ CTFontRef ctFont = (__bridge CTFontRef)systemFont; CFURLRef fontURL = (CFURLRef)CTFontCopyAttribute(ctFont, kCTFontURLAttribute); - if (!fontURL) - throw std::runtime_error("Failed to get font URL from system font"); - // __bridge_transfer transfers ownership to ARC, so no manual CFRelease is needed - NSString* fontPath = (NSString*)CFURLCopyFileSystemPath(fontURL, kCFURLPOSIXPathStyle); - if (!fontPath) - throw std::runtime_error("Failed to convert font URL to file path"); + std::string fontPath; - // Read the font file data - try - { - fontData.fontdata = ReadAllBytes(std::string([fontPath UTF8String])); - } - catch (const std::exception& e) + if (fontURL) { + CFStringRef cfPath = CFURLCopyFileSystemPath(fontURL, kCFURLPOSIXPathStyle); + if (cfPath) + { + fontPath = std::string([(NSString*)cfPath UTF8String]); + CFRelease(cfPath); + } CFRelease(fontURL); - throw std::runtime_error(std::string("Error reading system font file: ") + e.what()); } - catch (...) + + // Fallback to known macOS monospace font paths if system font URL lookup fails + if (fontPath.empty()) { - CFRelease(fontURL); - throw; + // Try common macOS monospace font locations + const char* fallbackPaths[] = { + "/System/Library/Fonts/SFNSMono.ttf", // SF Mono (newer macOS) + "/System/Library/Fonts/Monaco.ttf", // Monaco + "/Library/Fonts/Courier New.ttf", // Courier New + "/System/Library/Fonts/Courier.dfont", // Courier fallback + "/Library/Fonts/Menlo.ttc", // Menlo + }; + + for (const char* path : fallbackPaths) + { + std::ifstream test(path); + if (test.good()) + { + fontPath = path; + break; + } + } } - CFRelease(fontURL); - // fontPath is __bridge_transfer, so it's autoreleased(ARC) + if (fontPath.empty()) + throw std::runtime_error("Could not find monospace system font"); + + fontData.fontdata = ReadAllBytes(fontPath); } return { std::move(fontData) }; } diff --git a/src/core/resourcedata_unix.cpp b/src/core/resourcedata_unix.cpp index 6749335..817f1a7 100644 --- a/src/core/resourcedata_unix.cpp +++ b/src/core/resourcedata_unix.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include static std::vector ReadAllBytes(const std::string& filename) @@ -23,24 +22,18 @@ static std::vector ReadAllBytes(const std::string& filename) return buffer; } -static std::vector GetGtkUIFont(const std::string& propertyName) +// Use fontconfig directly instead of GTK to find system fonts +// This works on all Linux desktop environments (GNOME, KDE, XFCE, etc.) +// and doesn't require GTK/GNOME desktop settings +static std::vector GetSystemFont(const char* fcName) { - // Ask GTK what the UI font is: - - GSettings *settings = g_settings_new ("org.gnome.desktop.interface"); - gchar* str = g_settings_get_string(settings, propertyName.c_str()); - if (!str) - throw std::runtime_error("Could not get gtk font property"); - std::string fontname = str; - g_free(str); - - // Find the font filename using fontconfig: - std::string filename; + + // Use fontconfig to find the font FcConfig* config = FcInitLoadConfigAndFonts(); if (config) { - FcPattern* pat = FcNameParse((const FcChar8*)(fontname.c_str())); + FcPattern* pat = FcNameParse((const FcChar8*)fcName); if (pat) { FcConfigSubstitute(config, pat, FcMatchPattern); @@ -56,9 +49,20 @@ static std::vector GetGtkUIFont(const std::string& propertyName) } FcPatternDestroy(pat); } + FcConfigDestroy(config); + } + + // Fallback to DejaVu fonts if fontconfig fails + if (filename.empty()) + { + if (std::string(fcName) == "monospace") + filename = "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"; + else + filename = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"; } + if (filename.empty()) - throw std::runtime_error("Could not find font filename for: " + fontname); + throw std::runtime_error(std::string("Could not find font: ") + fcName); SingleFontData fontdata; fontdata.fontdata = ReadAllBytes(filename); @@ -67,12 +71,12 @@ static std::vector GetGtkUIFont(const std::string& propertyName) std::vector ResourceData::LoadSystemFont() { - return GetGtkUIFont("font-name"); + return GetSystemFont("sans-serif"); } std::vector ResourceData::LoadMonospaceSystemFont() { - return GetGtkUIFont("monospace-font-name"); + return GetSystemFont("monospace"); } double ResourceData::GetSystemFontSize() diff --git a/src/core/truetypefont.cpp b/src/core/truetypefont.cpp index bfc4b4d..4bbb377 100644 --- a/src/core/truetypefont.cpp +++ b/src/core/truetypefont.cpp @@ -89,7 +89,6 @@ TTCFontName TrueTypeFont::GetFontName() const TrueTypeTextMetrics TrueTypeFont::GetTextMetrics(double height) const { double scale = height / head.unitsPerEm; - double internalLeading = height - os2.sTypoAscender * scale + os2.sTypoDescender * scale; TrueTypeTextMetrics metrics; if (os2.usWinAscent != 0 || os2.usWinDescent != 0) @@ -1013,9 +1012,9 @@ void TTF_TableDirectory::Load(TrueTypeFileReader& reader) 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(); + reader.ReadUInt16(); // searchRange + reader.ReadUInt16(); // entrySelector + reader.ReadUInt16(); // rangeShift for (ttf_uint16 i = 0; i < numTables; i++) { @@ -2025,16 +2024,13 @@ TrueTypeGlyph TrueTypeFont::LoadCFFGlyph(uint32_t glyphIndex, double height) con double scaleY = -1.0; 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; } if (glyphIndex >= cff.CharStrings.size()) @@ -2056,7 +2052,6 @@ TrueTypeGlyph TrueTypeFont::LoadCFFGlyph(uint32_t glyphIndex, double height) con bool endchar = false; bool widthArg = true; - double widthValue = cff.PrivateDict.defaultWidthX; PathPoint cur, cp1, cp2, flex1start; while (!endchar) { @@ -2097,7 +2092,7 @@ TrueTypeGlyph TrueTypeFont::LoadCFFGlyph(uint32_t glyphIndex, double height) con if (oper == 12) oper = 1200 + (int)reader.ReadCard8(); - double tmp, fd; + double tmp; switch (oper) { // Path construction: @@ -2106,7 +2101,6 @@ TrueTypeGlyph TrueTypeFont::LoadCFFGlyph(uint32_t glyphIndex, double height) con { if (operands.size() > 2) { - widthValue = cff.PrivateDict.norminalWidthX + operands.get(0); operands.pop_front(); } widthArg = false; @@ -2122,7 +2116,6 @@ TrueTypeGlyph TrueTypeFont::LoadCFFGlyph(uint32_t glyphIndex, double height) con { if (operands.size() > 1) { - widthValue = cff.PrivateDict.norminalWidthX + operands.get(0); operands.pop_front(); } widthArg = false; @@ -2137,7 +2130,6 @@ TrueTypeGlyph TrueTypeFont::LoadCFFGlyph(uint32_t glyphIndex, double height) con { if (operands.size() > 1) { - widthValue = cff.PrivateDict.norminalWidthX + operands.get(0); operands.pop_front(); } widthArg = false; @@ -2461,7 +2453,7 @@ TrueTypeGlyph TrueTypeFont::LoadCFFGlyph(uint32_t glyphIndex, double height) con break; case 1235: // flex // To do: collapse to line when the flex depth is less than fd/100 device pixels - fd = operands.get(12); + // fd = operands.get(12); // TODO: implement flex depth check cur.x += operands.get(0); cur.y += operands.get(1); cp1 = cur; @@ -2556,7 +2548,6 @@ TrueTypeGlyph TrueTypeFont::LoadCFFGlyph(uint32_t glyphIndex, double height) con { if (operands.size() % 2 == 1) { - widthValue = cff.PrivateDict.norminalWidthX + operands.get(0); operands.pop_front(); } widthArg = false; @@ -2570,7 +2561,6 @@ TrueTypeGlyph TrueTypeFont::LoadCFFGlyph(uint32_t glyphIndex, double height) con { if (operands.size() % 2 == 1) { - widthValue = cff.PrivateDict.norminalWidthX + operands.get(0); operands.pop_front(); } widthArg = false; @@ -2591,7 +2581,6 @@ TrueTypeGlyph TrueTypeFont::LoadCFFGlyph(uint32_t glyphIndex, double height) con { if (operands.size() % 2 == 1) { - widthValue = cff.PrivateDict.norminalWidthX + operands.get(0); operands.pop_front(); } widthArg = false; @@ -2612,7 +2601,6 @@ TrueTypeGlyph TrueTypeFont::LoadCFFGlyph(uint32_t glyphIndex, double height) con { if (operands.size() % 2 == 1) { - widthValue = cff.PrivateDict.norminalWidthX + operands.get(0); operands.pop_front(); } widthArg = false; @@ -2633,7 +2621,6 @@ TrueTypeGlyph TrueTypeFont::LoadCFFGlyph(uint32_t glyphIndex, double height) con { if (operands.size() % 2 == 1) { - widthValue = cff.PrivateDict.norminalWidthX + operands.get(0); operands.pop_front(); } widthArg = false; diff --git a/src/core/truetypefont.h b/src/core/truetypefont.h index b3dcf42..dda64dc 100644 --- a/src/core/truetypefont.h +++ b/src/core/truetypefont.h @@ -15,6 +15,8 @@ class TTFDataBuffer static std::shared_ptr create(size_t size); static std::shared_ptr create(std::vector buffer); + virtual ~TTFDataBuffer() = default; + virtual char* data() = 0; virtual const char* data() const = 0; virtual size_t size() const = 0; diff --git a/src/core/widget.cpp b/src/core/widget.cpp index cad57e0..2ade4a2 100644 --- a/src/core/widget.cpp +++ b/src/core/widget.cpp @@ -6,6 +6,7 @@ #include #include #include +#include Widget::Widget(Widget* parent, WidgetType type, RenderAPI renderAPI) : Type(type) { @@ -34,6 +35,13 @@ Widget::Widget(Widget* parent, WidgetType type, RenderAPI renderAPI) : Type(type Widget::~Widget() { + // Release cursor lock if this widget has it + Widget* window = Window(); + if (window && window->CursorLockWidget == this) + { + window->CursorLockWidget = nullptr; + } + for (auto subscription: Subscriptions) subscription->Subscribers.erase(this); @@ -398,7 +406,10 @@ void Widget::Update() void Widget::Repaint() { Widget* w = Window(); - if (!w || !w->DispCanvas) + if (!w) + return; + + if (!w->DispCanvas) return; Canvas* canvas = w->DispCanvas.get(); @@ -422,7 +433,9 @@ void Widget::Paint(Canvas* canvas) for (Widget* w = FirstChild(); w != nullptr; w = w->NextSibling()) { if (w->Type == WidgetType::Child && !w->HiddenFlag) + { w->Paint(canvas); + } } canvas->setOrigin(oldOrigin); canvas->popClip(); @@ -680,11 +693,19 @@ Widget* Widget::ChildAt(const Point& pos) Point Widget::MapFrom(const Widget* parent, const Point& pos) const { Point p = pos; + int iterations = 0; for (const Widget* cur = this; cur != nullptr; cur = cur->Parent()) { + iterations++; if (cur == parent) + { return p; + } p -= cur->ContentGeometry.topLeft(); + if (iterations > 100) + { + throw std::runtime_error("MapFrom: infinite loop detected"); + } } throw std::runtime_error("MapFrom: not a parent of widget"); } @@ -791,18 +812,37 @@ void Widget::OnWindowMouseDown(const Point& pos, InputKey key) { if (CursorLockWidget) { - CursorLockWidget->OnMouseDown(CursorLockWidget->MapFrom(this, pos), key); + try + { + Point mappedPos = CursorLockWidget->MapFrom(this, pos); + CursorLockWidget->OnMouseDown(mappedPos, key); + } + catch (const std::exception& e) + { + // Silently handle MapFrom errors for cursor-locked widgets + } } else { Widget* widget = ChildAt(pos); + if (!widget) widget = this; + while (widget) { - bool stopPropagation = widget->OnMouseDown(widget->MapFrom(this, pos), key); - if (stopPropagation || widget == this) + try + { + Point mappedPos = widget->MapFrom(this, pos); + bool stopPropagation = widget->OnMouseDown(mappedPos, key); + + if (stopPropagation || widget == this) + break; + } + catch (const std::exception& e) + { break; + } widget = widget->Parent(); } } @@ -833,7 +873,15 @@ void Widget::OnWindowMouseUp(const Point& pos, InputKey key) { if (CursorLockWidget) { - CursorLockWidget->OnMouseUp(CursorLockWidget->MapFrom(this, pos), key); + try + { + Point mappedPos = CursorLockWidget->MapFrom(this, pos); + CursorLockWidget->OnMouseUp(mappedPos, key); + } + catch (const std::exception& e) + { + // Silently handle MapFrom errors for cursor-locked widgets + } } else { @@ -842,9 +890,17 @@ void Widget::OnWindowMouseUp(const Point& pos, InputKey key) widget = this; while (widget) { - bool stopPropagation = widget->OnMouseUp(widget->MapFrom(this, pos), key); - if (stopPropagation || widget == this) + try + { + Point mappedPos = widget->MapFrom(this, pos); + bool stopPropagation = widget->OnMouseUp(mappedPos, key); + if (stopPropagation || widget == this) + break; + } + catch (const std::exception& e) + { break; + } widget = widget->Parent(); } } diff --git a/src/widgets/dropdown/dropdown.cpp b/src/widgets/dropdown/dropdown.cpp index 2a0e43b..ba6980c 100644 --- a/src/widgets/dropdown/dropdown.cpp +++ b/src/widgets/dropdown/dropdown.cpp @@ -13,6 +13,13 @@ Dropdown::Dropdown(Widget* parent) : Widget(parent) p->Subscribe(this); } +Dropdown::~Dropdown() +{ + // Clean up any pending dropdown deletion + delete pendingDeleteDropdown; + pendingDeleteDropdown = nullptr; +} + DropdownList::DropdownList(Widget* parent, Dropdown* owner) : ListView(parent), owner(owner) { } @@ -299,12 +306,20 @@ void Dropdown::OnLostFocus() bool Dropdown::OpenDropdown() { - if (dropdownOpen || items.empty()) return false; + if (dropdownOpen || items.empty()) + { + return false; + } + + // Clean up any pending dropdown deletion from previous close + delete pendingDeleteDropdown; + pendingDeleteDropdown = nullptr; dropdownOpen = true; dropdown = new Widget(Window()); listView = new DropdownList(dropdown, this); + for (const auto& item : items) { listView->AddItem(item); @@ -321,10 +336,9 @@ bool Dropdown::OpenDropdown() GetWidth() + GetNoncontentLeft() + GetNoncontentRight(), GetDisplayItems() * 20.0 + 20 ); - OnGeometryChanged(); + OnGeometryChanged(); dropdown->Show(); - listView->ScrollToItem(selectedItem); return true; @@ -332,12 +346,23 @@ bool Dropdown::OpenDropdown() bool Dropdown::CloseDropdown() { - if (!dropdownOpen || !dropdown) return false; + if (!dropdownOpen || !dropdown) + { + return false; + } - dropdown->Close(); + // Hide the dropdown immediately and mark as closed + // This prevents it from receiving further mouse events + dropdown->SetVisible(false); + dropdownOpen = false; + + // Mark dropdown for deferred deletion - we'll delete it when: + // 1. The Dropdown itself is destroyed + // 2. A new dropdown is opened + // This avoids deleting widgets during event processing + pendingDeleteDropdown = dropdown; dropdown = nullptr; listView = nullptr; - dropdownOpen = false; Update(); @@ -348,8 +373,10 @@ void Dropdown::OnDropdownActivated() { if (listView) { - SetSelectedItem(listView->GetSelectedItem()); + int selectedItem = listView->GetSelectedItem(); + SetSelectedItem(selectedItem); } + CloseDropdown(); SetFocus(); } diff --git a/src/widgets/listview/listview.cpp b/src/widgets/listview/listview.cpp index bb74f58..b606863 100644 --- a/src/widgets/listview/listview.cpp +++ b/src/widgets/listview/listview.cpp @@ -98,7 +98,9 @@ void ListView::RemoveItem(int index) void ListView::Activate() { if (OnActivated) + { OnActivated(); + } } void ListView::SetSelectedItem(int index) @@ -107,8 +109,11 @@ void ListView::SetSelectedItem(int index) { selectedItem = index; Update(); + if (OnChanged) + { OnChanged(selectedItem); + } } } @@ -188,7 +193,10 @@ bool ListView::OnMouseDown(const Point& pos, InputKey key) if (key == InputKey::LeftMouse) { - int index = (int)((pos.y - 5.0 + scrollbar->GetPosition()) / getItemHeight()); + double itemHeight = getItemHeight(); + double scrollPos = scrollbar->GetPosition(); + int index = (int)((pos.y - 5.0 + scrollPos) / itemHeight); + if (index >= 0 && (size_t)index < items.size()) { ScrollToItem(index); diff --git a/src/widgets/tabwidget/tabwidget.cpp b/src/widgets/tabwidget/tabwidget.cpp index 7f49c86..d6259a5 100644 --- a/src/widgets/tabwidget/tabwidget.cpp +++ b/src/widgets/tabwidget/tabwidget.cpp @@ -172,10 +172,14 @@ void TabBar::SetCurrentIndex(int pageIndex) if (CurrentIndex != pageIndex) { if (CurrentIndex != -1) + { Tabs[CurrentIndex]->SetCurrent(false); + } CurrentIndex = pageIndex; if (CurrentIndex != -1) + { Tabs[CurrentIndex]->SetCurrent(true); + } } } @@ -186,7 +190,9 @@ void TabBar::OnTabClicked(TabBarTab* tab) { SetCurrentIndex(pageIndex); if (OnCurrentChanged) + { OnCurrentChanged(); + } } } @@ -314,7 +320,9 @@ void TabBarTab::OnMouseMove(const Point& pos) bool TabBarTab::OnMouseDown(const Point& pos, InputKey key) { if (OnClick) + { OnClick(); + } return true; } diff --git a/src/widgets/textedit/textedit.cpp b/src/widgets/textedit/textedit.cpp index d25d56a..9cd35f6 100644 --- a/src/widgets/textedit/textedit.cpp +++ b/src/widgets/textedit/textedit.cpp @@ -296,7 +296,9 @@ bool TextEdit::OnMouseDown(const Point& pos, InputKey key) { SetPointerCapture(); mouse_selecting = true; + cursor_pos = GetCharacterIndex(pos); + selection_start = cursor_pos; selection_length = 0; @@ -554,17 +556,35 @@ void TextEdit::OnKeyUp(InputKey key) void TextEdit::OnSetFocus() { if (!readonly) + { timer->Start(500); + } + if (select_all_on_focus_gain) + { SelectAll(); + } + ignore_mouse_events = true; - cursor_pos.y = lines.size() - 1; - cursor_pos.x = lines[cursor_pos.y].text.length(); + + // Safety check: ensure lines is not empty before accessing + if (!lines.empty()) + { + cursor_pos.y = lines.size() - 1; + cursor_pos.x = lines[cursor_pos.y].text.length(); + } + else + { + cursor_pos.x = 0; + cursor_pos.y = 0; + } Update(); if (FuncFocusGained) + { FuncFocusGained(); + } } void TextEdit::OnLostFocus() @@ -907,6 +927,12 @@ bool TextEdit::InputMaskAcceptsInput(ivec2 cursor_pos, const std::string& str) std::string::size_type TextEdit::ToOffset(ivec2 pos) const { + if (lines.empty()) + return 0; + + if (pos.y < 0) + return 0; + if (pos.y < (int)lines.size()) { std::string::size_type offset = 0; @@ -914,7 +940,7 @@ std::string::size_type TextEdit::ToOffset(ivec2 pos) const { offset += lines[line].text.size() + 1; } - return offset + std::min((size_t)pos.x, lines[pos.y].text.size()); + return offset + std::min((size_t)std::max(0, pos.x), lines[pos.y].text.size()); } else { @@ -923,12 +949,15 @@ std::string::size_type TextEdit::ToOffset(ivec2 pos) const { offset += lines[line].text.size() + 1; } - return offset - 1; + return offset > 0 ? offset - 1 : 0; } } TextEdit::ivec2 TextEdit::FromOffset(std::string::size_type offset) const { + if (lines.empty()) + return ivec2(0, 0); + int line_offset = 0; for (int line = 0; line < (int)lines.size(); line++) { @@ -1027,7 +1056,19 @@ void TextEdit::OnPaint(Canvas* canvas) TextEdit::ivec2 TextEdit::GetCharacterIndex(Point mouse_wincoords) { + // Safety check: handle empty lines + if (lines.empty()) + { + return ivec2(0, 0); + } + + // Safety check: handle null canvas Canvas* canvas = GetCanvas(); + if (!canvas) + { + return ivec2(0, 0); + } + for (size_t i = 0; i < lines.size(); i++) { Line& line = lines[i]; @@ -1047,5 +1088,6 @@ TextEdit::ivec2 TextEdit::GetCharacterIndex(Point mouse_wincoords) } } + // lines is guaranteed non-empty here due to check above return ivec2(lines.back().text.size(), lines.size() - 1); } diff --git a/src/window/cocoa/cocoa_display_backend.mm b/src/window/cocoa/cocoa_display_backend.mm index 6a68d7b..8dc0df3 100644 --- a/src/window/cocoa/cocoa_display_backend.mm +++ b/src/window/cocoa/cocoa_display_backend.mm @@ -8,29 +8,36 @@ #include "AppKitWrapper.h" +// Helper struct to hold timer callbacks without requiring a Widget owner +struct CocoaTimerData +{ + std::function callback; + NSTimer* nstimer; +}; + @interface ZWidgetTimerTarget : NSObject { - Timer* timer; + CocoaTimerData* timerData; } -- (id)initWithTimer:(Timer*)t; +- (id)initWithTimerData:(CocoaTimerData*)data; - (void)timerFired:(NSTimer*)nstimer; @end @implementation ZWidgetTimerTarget -- (id)initWithTimer:(Timer*)t; +- (id)initWithTimerData:(CocoaTimerData*)data { self = [super init]; if (self) { - timer = (Timer*)t; + timerData = data; } return self; } - (void)timerFired:(NSTimer*)nstimer { - if (timer->FuncExpired) - timer->FuncExpired(); + if (timerData && timerData->callback) + timerData->callback(); } @end @@ -42,6 +49,10 @@ - (void)timerFired:(NSTimer*)nstimer CocoaDisplayBackend::CocoaDisplayBackend() { + // CRITICAL: Initialize NSApp and set activation policy for keyboard events + [NSApplication sharedApplication]; + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + [NSApp finishLaunching]; } CocoaDisplayBackend::~CocoaDisplayBackend() @@ -70,26 +81,47 @@ - (void)timerFired:(NSTimer*)nstimer void CocoaDisplayBackend::ExitLoop() { [NSApp stop:nil]; + + // Post a dummy event to wake up the event loop so stop: takes effect + NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined + location:NSMakePoint(0, 0) + modifierFlags:0 + timestamp:0 + windowNumber:0 + context:nil + subtype:0 + data1:0 + data2:0]; + [NSApp postEvent:event atStart:YES]; } void* CocoaDisplayBackend::StartTimer(int timeoutMilliseconds, std::function onTimer) { - Timer* timer = new Timer(nullptr); - timer->FuncExpired = onTimer; - ZWidgetTimerTarget* target = [[ZWidgetTimerTarget alloc] initWithTimer:timer]; - NSTimer* nstimer = [NSTimer scheduledTimerWithTimeInterval:timeoutMilliseconds / 1000.0 target:target selector:@selector(timerFired:) userInfo:nil repeats:YES]; - timer->SetTimerId((__bridge void*)nstimer); // No retain needed with ARC - // [target release]; // No release needed with ARC - return timer; + CocoaTimerData* timerData = new CocoaTimerData(); + timerData->callback = onTimer; + + ZWidgetTimerTarget* target = [[ZWidgetTimerTarget alloc] initWithTimerData:timerData]; + NSTimer* nstimer = [NSTimer scheduledTimerWithTimeInterval:timeoutMilliseconds / 1000.0 + target:target + selector:@selector(timerFired:) + userInfo:nil + repeats:YES]; + timerData->nstimer = nstimer; + + return timerData; } void CocoaDisplayBackend::StopTimer(void* timerID) { - Timer* timer = static_cast(timerID); - NSTimer* nstimer = (__bridge NSTimer*)timer->GetTimerId(); - [nstimer invalidate]; - // [nstimer release]; // No release needed with ARC - delete timer; + CocoaTimerData* timerData = static_cast(timerID); + if (timerData) + { + if (timerData->nstimer) + { + [timerData->nstimer invalidate]; + } + delete timerData; + } } Size CocoaDisplayBackend::GetScreenSize() diff --git a/src/window/cocoa/cocoa_display_window.mm b/src/window/cocoa/cocoa_display_window.mm index ac91852..4337078 100644 --- a/src/window/cocoa/cocoa_display_window.mm +++ b/src/window/cocoa/cocoa_display_window.mm @@ -205,39 +205,72 @@ static CVReturn DisplayLinkOutputCallback(CVDisplayLinkRef displayLink, const CV @interface ZWidgetView : NSView { +@public CocoaDisplayWindowImpl* impl; + NSTrackingArea* trackingArea; } - (id)initWithImpl:(CocoaDisplayWindowImpl*)impl; @end @implementation ZWidgetView -+ (Class)layerClass -{ - return [CAMetalLayer class]; -} - - (id)initWithImpl:(CocoaDisplayWindowImpl*)d { self = [super init]; if (self) { impl = d; - self.wantsLayer = YES; - self.layer.contentsScale = [NSScreen mainScreen].backingScaleFactor; - [self addTrackingArea:[[NSTrackingArea alloc] initWithRect:self.bounds options:NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveInKeyWindow owner:self userInfo:nil]]; + // Only use layer-backing for Metal/OpenGL, not for Bitmap rendering + // For Bitmap rendering, we need drawRect: to be called + if (impl->renderAPI == RenderAPI::Metal || impl->renderAPI == RenderAPI::OpenGL) + { + self.wantsLayer = YES; + self.layer.contentsScale = [NSScreen mainScreen].backingScaleFactor; + } + // CRITICAL: Don't access self.layer for Bitmap rendering - accessing it creates a layer! + // Don't create tracking area here - it will be created in updateTrackingAreas + trackingArea = nil; } return self; } +- (void)dealloc +{ + if (trackingArea) { + [self removeTrackingArea:trackingArea]; + trackingArea = nil; + } +} + - (BOOL)isOpaque { return YES; } +- (void)updateTrackingAreas +{ + if (trackingArea) { + [self removeTrackingArea:trackingArea]; + trackingArea = nil; + } + + trackingArea = [[NSTrackingArea alloc] initWithRect:self.bounds + options:NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveInKeyWindow + owner:self + userInfo:nil]; + [self addTrackingArea:trackingArea]; + [super updateTrackingAreas]; +} + - (CALayer *)makeBackingLayer { - return [[self.class layerClass] layer]; + // This is only called for layer-backed views (Metal/OpenGL) + // Create appropriate layer based on render API + if (impl && impl->renderAPI == RenderAPI::Metal) + { + return [CAMetalLayer layer]; + } + return [CALayer layer]; // For OpenGL } - (BOOL)canBecomeKeyView { @@ -247,33 +280,57 @@ - (BOOL)canBecomeKeyView - (void)setFrame:(NSRect)frame { [super setFrame:frame]; - impl->updateDrawableSize(frame.size); +#ifdef HAVE_METAL + if (impl && impl->renderAPI == RenderAPI::Metal) + { + impl->updateDrawableSize(frame.size); + } +#endif } +- (NSView *)hitTest:(NSPoint)point +{ + return [super hitTest:point]; +} + - (BOOL)acceptsFirstResponder { return YES; } +- (BOOL)acceptsFirstMouse:(NSEvent *)event +{ + return YES; // Allow mouse events even if window isn't key +} + +- (BOOL)becomeFirstResponder +{ + return [super becomeFirstResponder]; +} + +- (BOOL)resignFirstResponder +{ + return [super resignFirstResponder]; +} + +- (void)setNeedsDisplay:(BOOL)flag +{ + [super setNeedsDisplay:flag]; +} + - (void)drawRect:(NSRect)dirtyRect { - NSLog(@"ZWidgetView: drawRect called"); if (!impl) return; - if (impl->renderAPI == RenderAPI::Bitmap) + if (impl->renderAPI == RenderAPI::Bitmap || impl->renderAPI == RenderAPI::Unspecified) { + // Just draw the existing bitmap (Update() handles regeneration asynchronously) if (impl->cgImage) { - NSLog(@"drawRect: impl->cgImage is NOT null. Drawing image."); CGContextRef context = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort]; - CGContextSaveGState(context); - CGContextTranslateCTM(context, 0, self.bounds.size.height); - CGContextScaleCTM(context, 1.0, -1.0); + if (!context) { + return; + } CGContextDrawImage(context, NSRectToCGRect(self.bounds), impl->cgImage); - CGContextRestoreGState(context); - } - else - { - NSLog(@"drawRect: impl->cgImage IS null. Not drawing image."); } } else if (impl->renderAPI == RenderAPI::OpenGL) @@ -299,9 +356,16 @@ - (void)viewDidMoveToWindow { if ([self window]) { - NSLog(@"ZWidgetView: viewDidMoveToWindow with windowHost: %p", (void*)impl->windowHost); + // Make this view the first responder to receive mouse/keyboard events + [[self window] makeFirstResponder:self]; + impl->windowHost->OnWindowPaint(); - impl->startDisplayLink(); + // Only start displayLink for OpenGL/Metal rendering (continuous refresh needed) + // Bitmap rendering is event-driven, no continuous refresh needed + if (impl->renderAPI == RenderAPI::OpenGL || impl->renderAPI == RenderAPI::Metal) + { + impl->startDisplayLink(); + } } } @@ -345,6 +409,8 @@ - (void)mouseDown:(NSEvent *)theEvent if (impl && impl->windowHost) { NSPoint p = [theEvent locationInWindow]; + double flippedY = [self frame].size.height - p.y; + InputKey mouseKey = InputKey::None; if ([theEvent buttonNumber] == 0) mouseKey = InputKey::LeftMouse; else if ([theEvent buttonNumber] == 1) mouseKey = InputKey::RightMouse; @@ -352,7 +418,7 @@ - (void)mouseDown:(NSEvent *)theEvent if (mouseKey != InputKey::None) { - impl->windowHost->OnWindowMouseDown(Point(p.x, [self frame].size.height - p.y), mouseKey); + impl->windowHost->OnWindowMouseDown(Point(p.x, flippedY), mouseKey); } } } @@ -362,6 +428,8 @@ - (void)mouseUp:(NSEvent *)theEvent if (impl && impl->windowHost) { NSPoint p = [theEvent locationInWindow]; + double flippedY = [self frame].size.height - p.y; + InputKey mouseKey = InputKey::None; if ([theEvent buttonNumber] == 0) mouseKey = InputKey::LeftMouse; else if ([theEvent buttonNumber] == 1) mouseKey = InputKey::RightMouse; @@ -369,7 +437,7 @@ - (void)mouseUp:(NSEvent *)theEvent if (mouseKey != InputKey::None) { - impl->windowHost->OnWindowMouseUp(Point(p.x, [self frame].size.height - p.y), mouseKey); + impl->windowHost->OnWindowMouseUp(Point(p.x, flippedY), mouseKey); } } } @@ -431,15 +499,28 @@ - (void)keyDown:(NSEvent *)theEvent { InputKey key = keycode_to_inputkey([theEvent keyCode]); impl->keyState[key] = true; - impl->windowHost->OnWindowKeyDown(key); // Removed isARepeat as it's not in the ZWidget API - - NSString* characters = [theEvent characters]; - if ([characters length] > 0) - { - impl->windowHost->OnWindowKeyChar([characters UTF8String]); - } + impl->windowHost->OnWindowKeyDown(key); } + // Call interpretKeyEvents to trigger insertText: for text input + [self interpretKeyEvents:@[theEvent]]; +} + +// Text input protocol methods - required for keyboard input on macOS +- (void)insertText:(id)string +{ + if (impl && impl->windowHost) + { + NSString* str = [string isKindOfClass:[NSAttributedString class]] ? + [(NSAttributedString*)string string] : (NSString*)string; + impl->windowHost->OnWindowKeyChar([str UTF8String]); + } +} + +- (void)doCommandBySelector:(SEL)selector +{ + // This handles special keys like Enter, Tab, etc. + // They're already handled by keyDown, so we can ignore them here } - (void)keyUp:(NSEvent *)theEvent @@ -528,6 +609,29 @@ - (id)initWithImpl:(CocoaDisplayWindowImpl*)d } return self; } + +- (void)windowDidResize:(NSNotification *)notification +{ + // Throttle resize events to prevent excessive calls that can cause crashes + static int resizeCount = 0; + static NSTimeInterval lastResizeTime = 0; + NSTimeInterval now = [[NSDate date] timeIntervalSince1970]; + + if (now - lastResizeTime < 0.01) { + resizeCount++; + if (resizeCount > 5) { + return; // Throttle: too many rapid calls + } + } else { + resizeCount = 0; + } + lastResizeTime = now; + + if (impl && impl->windowHost) + { + impl->windowHost->OnWindowGeometryChanged(); + } +} @end void CocoaDisplayWindowImpl::initOpenGL(ZWidgetView* view) @@ -563,7 +667,8 @@ - (id)initWithImpl:(CocoaDisplayWindowImpl*)d CVReturn CocoaDisplayWindowImpl::displayLinkOutputCallback(CVDisplayLinkRef displayLink, const CVTimeStamp* inNow, const CVTimeStamp* inOutputTime, CVOptionFlags flagsIn, CVOptionFlags* flagsOut, void* displayLinkContext) { - NSLog(@"displayLinkOutputCallback called"); + // Note: displayLink is only started for OpenGL/Metal rendering + // Bitmap rendering is event-driven and doesn't use displayLink if (renderAPI == RenderAPI::Metal) { #ifdef HAVE_METAL @@ -584,14 +689,6 @@ - (id)initWithImpl:(CocoaDisplayWindowImpl*)d } #endif } - else if (renderAPI == RenderAPI::Bitmap) - { - NSLog(@"displayLinkOutputCallback: RenderAPI::Bitmap branch executed. Dispatching OnWindowPaint() to main thread."); - dispatch_async(dispatch_get_main_queue(), ^{ - NSLog(@"dispatch_async block executed. Calling OnWindowPaint()."); - if (windowHost) windowHost->OnWindowPaint(); - }); - } return kCVReturnSuccess; } @@ -624,22 +721,27 @@ - (id)initWithImpl:(CocoaDisplayWindowImpl*)d } #ifdef HAVE_METAL - // Create Metal device and layer for application rendering (not ZWidget rendering) - // Applications can access these via GetMetalDevice() and GetMetalLayer() - impl->metalDevice = MTLCreateSystemDefaultDevice(); - if (impl->metalDevice) - { - impl->metalLayer = [CAMetalLayer layer]; - impl->metalLayer.device = impl->metalDevice; - impl->metalLayer.pixelFormat = MTLPixelFormatBGRA8Unorm; - impl->metalLayer.framebufferOnly = NO; // Allow reading for screenshots, etc. - impl->metalLayer.presentsWithTransaction = NO; - impl->metalLayer.displaySyncEnabled = YES; - impl->metalLayer.maximumDrawableCount = 3; - impl->metalLayer.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable; - impl->metalLayer.contentsScale = [[NSScreen mainScreen] backingScaleFactor]; - impl->metalLayer.frame = view.layer.frame; - [view.layer addSublayer:impl->metalLayer]; + // Only create Metal layer if actually using Metal rendering + // Accessing view.layer forces layer-backing which breaks Bitmap rendering + if (renderAPI == RenderAPI::Metal) + { + // Create Metal device and layer for application rendering + // Applications can access these via GetMetalDevice() and GetMetalLayer() + impl->metalDevice = MTLCreateSystemDefaultDevice(); + if (impl->metalDevice) + { + impl->metalLayer = [CAMetalLayer layer]; + impl->metalLayer.device = impl->metalDevice; + impl->metalLayer.pixelFormat = MTLPixelFormatBGRA8Unorm; + impl->metalLayer.framebufferOnly = NO; // Allow reading for screenshots, etc. + impl->metalLayer.presentsWithTransaction = NO; + impl->metalLayer.displaySyncEnabled = YES; + impl->metalLayer.maximumDrawableCount = 3; + impl->metalLayer.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable; + impl->metalLayer.contentsScale = [[NSScreen mainScreen] backingScaleFactor]; + impl->metalLayer.frame = view.layer.frame; + [view.layer addSublayer:impl->metalLayer]; + } } #endif #ifdef HAVE_OPENGL @@ -656,7 +758,6 @@ - (id)initWithImpl:(CocoaDisplayWindowImpl*)d CocoaDisplayWindow::~CocoaDisplayWindow() { - NSLog(@"CocoaDisplayWindow: Destructor entered, this = %p", (void*)this); } void CocoaDisplayWindow::SetWindowTitle(const std::string& text) @@ -718,8 +819,19 @@ - (id)initWithImpl:(CocoaDisplayWindowImpl*)d { if (impl->window) { + // CRITICAL: Activate the application so it can receive keyboard events + [NSApp activateIgnoringOtherApps:YES]; [impl->window makeKeyAndOrderFront:nil]; - Update(); + + // Trigger initial layout + if (impl->windowHost) + { + impl->windowHost->OnWindowGeometryChanged(); + // Paint to create CGImage before any drawRect calls + impl->windowHost->OnWindowPaint(); + } + // Just trigger a redraw without regenerating (bitmap already created above) + [[impl->window contentView] setNeedsDisplay:YES]; } } @@ -837,10 +949,25 @@ - (id)initWithImpl:(CocoaDisplayWindowImpl*)d void CocoaDisplayWindow::Update() { - if (impl->window) - { - [[impl->window contentView] setNeedsDisplay:YES]; - } + if (!impl->window || !impl->windowHost) + return; + + // Queue paint asynchronously to avoid interfering with event processing + // Use weak reference to window to avoid dangling pointer issues + __weak NSWindow* weakWindow = impl->window; + DisplayWindowHost* hostPtr = impl->windowHost; + + dispatch_async(dispatch_get_main_queue(), ^{ + NSWindow* strongWindow = weakWindow; + if (!strongWindow) + return; + + // Regenerate the bitmap + hostPtr->OnWindowPaint(); + + // Trigger redraw + [[strongWindow contentView] setNeedsDisplay:YES]; + }); } bool CocoaDisplayWindow::GetKeyState(InputKey key) @@ -1014,9 +1141,8 @@ - (id)initWithImpl:(CocoaDisplayWindowImpl*)d } #endif } - else if (impl->renderAPI == RenderAPI::Bitmap) + else if (impl->renderAPI == RenderAPI::Bitmap || impl->renderAPI == RenderAPI::Unspecified) { - NSLog(@"PresentBitmap: RenderAPI::Bitmap path executed."); if (impl->cgImage) CGImageRelease(impl->cgImage); impl->cgImage = nullptr; @@ -1033,26 +1159,16 @@ - (id)initWithImpl:(CocoaDisplayWindowImpl*)d if (context) { impl->cgImage = CGBitmapContextCreateImage(context); - if (impl->cgImage) - { - NSLog(@"PresentBitmap: CGImageRef created successfully."); - } - else - { - NSLog(@"PresentBitmap: Failed to create CGImageRef."); - } CGContextRelease(context); } - else - { - NSLog(@"PresentBitmap: Failed to create CGBitmapContext."); - } CGColorSpaceRelease(colorSpace); if (impl->window) { dispatch_async(dispatch_get_main_queue(), ^{ - [[impl->window contentView] setNeedsDisplay:YES]; + NSView* contentView = [impl->window contentView]; + [contentView setNeedsDisplay:YES]; + [contentView displayIfNeeded]; }); } } diff --git a/src/window/cocoa/cocoa_open_file_dialog.h b/src/window/cocoa/cocoa_open_file_dialog.h index c01f8f5..8133045 100644 --- a/src/window/cocoa/cocoa_open_file_dialog.h +++ b/src/window/cocoa/cocoa_open_file_dialog.h @@ -35,4 +35,7 @@ class CocoaOpenFileDialog : public OpenFileDialog std::string _title; std::vector _filenames; void* panel = nullptr; + + // Helper method to run modal and collect results + bool runModalAndGetResults(); }; diff --git a/src/window/cocoa/cocoa_open_file_dialog.mm b/src/window/cocoa/cocoa_open_file_dialog.mm index 3bcf144..c0bdcb2 100644 --- a/src/window/cocoa/cocoa_open_file_dialog.mm +++ b/src/window/cocoa/cocoa_open_file_dialog.mm @@ -30,7 +30,7 @@ void CocoaOpenFileDialog::SetDefaultExtension(const std::string& extension) { - if (@available(macOS 11.0, *)) { + if (@available(macOS 12.0, *)) { NSString* extensionString = [NSString stringWithUTF8String:extension.c_str()]; UTType* utType = [UTType typeWithFilenameExtension:extensionString]; if (utType) { @@ -68,23 +68,57 @@ [((__bridge NSOpenPanel*)panel) setTitle:[NSString stringWithUTF8String:title.c_str()]]; } +// Helper to run modal and collect results +bool CocoaOpenFileDialog::runModalAndGetResults() +{ + if ([((__bridge NSOpenPanel*)panel) runModal] == NSModalResponseOK) + { + _filenames.clear(); + for (NSURL* url in [((__bridge NSOpenPanel*)panel) URLs]) + { + _filenames.push_back([[url path] UTF8String]); + } + return true; + } + return false; +} + bool CocoaOpenFileDialog::Show() { - if (@available(macOS 11.0, *)) { + // IMPORTANT: Allow selection of files that may not match the exact content type + [((__bridge NSOpenPanel*)panel) setAllowsOtherFileTypes:YES]; + + if (@available(macOS 12.0, *)) { if (!_filters.empty()) { NSArray* fileTypeStrings = [[NSString stringWithUTF8String:_filters[_filterIndex].second.c_str()] componentsSeparatedByString:@";"]; NSMutableArray* utTypes = [NSMutableArray array]; for (NSString* typeString in fileTypeStrings) { - UTType* utType = [UTType typeWithFilenameExtension:typeString]; + // Strip wildcard prefix (*.ext -> ext) + NSString* extension = typeString; + if ([extension hasPrefix:@"*."]) { + extension = [extension substringFromIndex:2]; + } else if ([extension hasPrefix:@"*"]) { + extension = [extension substringFromIndex:1]; + } + + // Handle special case for *.* + if ([extension isEqualToString:@".*"] || [extension isEqualToString:@"*"]) { + // Allow all file types - set empty array + [((__bridge NSOpenPanel*)panel) setAllowedContentTypes:@[]]; + return runModalAndGetResults(); + } + + UTType* utType = [UTType typeWithFilenameExtension:extension]; if (utType) { [utTypes addObject:utType]; } + // If UTType doesn't exist (like .wad), allowsOtherFileTypes will handle it } if ([utTypes count] > 0) { [((__bridge NSOpenPanel*)panel) setAllowedContentTypes:utTypes]; } else { - // Fallback if no valid UTTypes could be created + // Fallback if no valid UTTypes could be created - allow all files [((__bridge NSOpenPanel*)panel) setAllowedContentTypes:@[]]; } } @@ -96,25 +130,34 @@ if (!_filters.empty()) { NSArray* fileTypeStrings = [[NSString stringWithUTF8String:_filters[_filterIndex].second.c_str()] componentsSeparatedByString:@";"]; - [((__bridge NSOpenPanel*)panel) setAllowedFileTypes:fileTypeStrings]; + NSMutableArray* cleanExtensions = [NSMutableArray array]; + for (NSString* typeString in fileTypeStrings) { + // Strip wildcard prefix (*.ext -> ext) + NSString* extension = typeString; + if ([extension hasPrefix:@"*."]) { + extension = [extension substringFromIndex:2]; + } else if ([extension hasPrefix:@"*"]) { + extension = [extension substringFromIndex:1]; + } + + // Handle special case for *.* + if ([extension isEqualToString:@".*"] || [extension isEqualToString:@"*"]) { + // Allow all file types - set nil + [((__bridge NSOpenPanel*)panel) setAllowedFileTypes:nil]; + return runModalAndGetResults(); + } + + [cleanExtensions addObject:extension]; + } + [((__bridge NSOpenPanel*)panel) setAllowedFileTypes:cleanExtensions]; } else { - [((__bridge NSOpenPanel*)panel) setAllowedFileTypes:@[]]; // No filters + [((__bridge NSOpenPanel*)panel) setAllowedFileTypes:nil]; // No filters - allow all } } - if ([((__bridge NSOpenPanel*)panel) runModal] == NSModalResponseOK) - { - _filenames.clear(); - - for (NSURL* url in [((__bridge NSOpenPanel*)panel) URLs]) - { - _filenames.push_back([[url path] UTF8String]); - } - return true; - } - return false; + return runModalAndGetResults(); } std::string CocoaOpenFileDialog::Filename() const diff --git a/src/window/cocoa/cocoa_save_file_dialog.mm b/src/window/cocoa/cocoa_save_file_dialog.mm index 69d630f..e42da0c 100644 --- a/src/window/cocoa/cocoa_save_file_dialog.mm +++ b/src/window/cocoa/cocoa_save_file_dialog.mm @@ -9,7 +9,7 @@ void CocoaSaveFileDialog::AddFilter(const std::string &filter_description, const std::string &filter_extension) { - if (@available(macOS 11.0, *)) { + if (@available(macOS 12.0, *)) { NSArray* fileTypeStrings = [[NSString stringWithUTF8String:filter_extension.c_str()] componentsSeparatedByString:@";"]; NSMutableArray* utTypes = [NSMutableArray array]; for (NSString* typeString in fileTypeStrings) { @@ -40,7 +40,7 @@ void CocoaSaveFileDialog::SetDefaultExtension(const std::string& extension) { - if (@available(macOS 11.0, *)) { + if (@available(macOS 12.0, *)) { NSString* extensionString = [NSString stringWithUTF8String:extension.c_str()]; UTType* utType = [UTType typeWithFilenameExtension:extensionString]; if (utType) { @@ -77,7 +77,7 @@ void CocoaSaveFileDialog::ClearFilters() { - if (@available(macOS 11.0, *)) { + if (@available(macOS 12.0, *)) { [((__bridge NSSavePanel*)panel) setAllowedContentTypes:@[]]; } else { [((__bridge NSSavePanel*)panel) setAllowedFileTypes:@[]]; diff --git a/src/window/window.cpp b/src/window/window.cpp index 5ff1117..bf1b5f8 100644 --- a/src/window/window.cpp +++ b/src/window/window.cpp @@ -93,6 +93,10 @@ std::unique_ptr DisplayBackend::TryCreateBackend() { backend = TryCreateWin32(); } + else if (backendSelectionStr == "Cocoa") + { + backend = TryCreateCocoa(); + } else if (backendSelectionStr == "X11") { backend = TryCreateX11(); @@ -110,6 +114,7 @@ std::unique_ptr DisplayBackend::TryCreateBackend() if (!backend) { backend = TryCreateWin32(); + if (!backend) backend = TryCreateCocoa(); if (!backend) backend = TryCreateWayland(); if (!backend) backend = TryCreateX11(); if (!backend) backend = TryCreateSDL3(); @@ -223,9 +228,15 @@ std::unique_ptr DisplayBackend::TryCreateWayland() #endif -#ifndef __APPLE__ +#ifdef __APPLE__ + +#include "cocoa/cocoa_display_backend.h" + +// DisplayBackend::TryCreateCocoa() is defined in cocoa_display_backend.mm + +#else -std::unique_ptr TryCreateCocoa() +std::unique_ptr DisplayBackend::TryCreateCocoa() { return nullptr; } From 7e61fa389e1b8c5e2538129649300636202e47a4 Mon Sep 17 00:00:00 2001 From: John Curley Date: Sat, 15 Nov 2025 20:35:42 +1300 Subject: [PATCH 2/3] fallback for monospace font not being available on older macOS versions --- src/core/resourcedata_mac.mm | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/core/resourcedata_mac.mm b/src/core/resourcedata_mac.mm index 6b2aee0..ad9bd44 100644 --- a/src/core/resourcedata_mac.mm +++ b/src/core/resourcedata_mac.mm @@ -76,12 +76,18 @@ SingleFontData fontData; @autoreleasepool { - NSFont* systemFont = [NSFont monospacedSystemFontOfSize:13.0 weight:NSFontWeightRegular]; // Use a default size - if (!systemFont) - throw std::runtime_error("Failed to get system font"); - - CTFontRef ctFont = (__bridge CTFontRef)systemFont; - CFURLRef fontURL = (CFURLRef)CTFontCopyAttribute(ctFont, kCTFontURLAttribute); + NSFont* systemFont = nil; + if (@available(macOS 10.15, *)) { + systemFont = [NSFont monospacedSystemFontOfSize:13.0 weight:NSFontWeightRegular]; // Use a default size + } + if (!systemFont) { + // Fallback for older macOS versions + systemFont = [NSFont systemFontOfSize:13.0]; + } + if (!systemFont) // Double check after fallback + throw std::runtime_error("Failed to get system font"); + + CTFontRef ctFont = (__bridge CTFontRef)systemFont; CFURLRef fontURL = (CFURLRef)CTFontCopyAttribute(ctFont, kCTFontURLAttribute); std::string fontPath; From 8471d2fdfc96f974a0fc91642f2a9f35aa3840ca Mon Sep 17 00:00:00 2001 From: John Curley Date: Sun, 16 Nov 2025 17:55:02 +1300 Subject: [PATCH 3/3] Removed excess and unnecessary changes for macOS builds --- CMakeLists.txt | 22 ++-- include/zwidget/widgets/dropdown/dropdown.h | 2 - src/core/canvas.cpp | 5 - src/core/resourcedata_mac.mm | 134 ++++++++------------ src/core/resourcedata_unix.cpp | 38 +++--- src/core/truetypefont.cpp | 23 +++- src/core/truetypefont.h | 2 - src/core/widget.cpp | 70 +--------- src/widgets/dropdown/dropdown.cpp | 41 +----- src/widgets/listview/listview.cpp | 10 +- src/widgets/tabwidget/tabwidget.cpp | 8 -- src/widgets/textedit/textedit.cpp | 50 +------- 12 files changed, 116 insertions(+), 289 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 64aaa47..221927b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,6 @@ project(zwidget) option(ENABLE_METAL "Enable Metal support for application rendering" ON) option(ENABLE_OPENGL "Enable OpenGL support for application rendering" ON) -option(ENABLE_SDL_ON_MACOS "Enable SDL backends on macOS (native Cocoa is preferred)" OFF) if (UNIX AND NOT APPLE) include(FindPkgConfig) @@ -338,20 +337,19 @@ else() set(ZWIDGET_DEFINES ${ZWIDGET_DEFINES} -DUSE_WAYLAND) endif() - # Note: GTK/GIO no longer required - using fontconfig directly - # set(ZWIDGET_INCLUDE_DIRS ${ZWIDGET_INCLUDE_DIRS} ${GLIB_INCLUDE_DIRS}) - # set(ZWIDGET_LIBS ${ZWIDGET_LIBS} ${GLIB_LDFLAGS} -lgio-2.0) + set(ZWIDGET_INCLUDE_DIRS ${ZWIDGET_INCLUDE_DIRS} ${GLIB_INCLUDE_DIRS}) + set(ZWIDGET_LIBS ${ZWIDGET_LIBS} ${GLIB_LDFLAGS} -lgio-2.0) set(ZWIDGET_INCLUDE_DIRS ${ZWIDGET_INCLUDE_DIRS} ${FONTCONFIG_INCLUDE_DIRS}) set(ZWIDGET_LIBS ${ZWIDGET_LIBS} ${FONTCONFIG_LDFLAGS}) endif() -if(SDL3_FOUND AND NOT WIN32 AND (NOT APPLE OR ENABLE_SDL_ON_MACOS)) +if(SDL3_FOUND AND NOT WIN32) set(ZWIDGET_SOURCES ${ZWIDGET_SOURCES} ${ZWIDGET_SDL3_SOURCES}) set(ZWIDGET_DEFINES ${ZWIDGET_DEFINES} -DUSE_SDL3) endif() -if(SDL2_FOUND AND NOT SDL3_FOUND AND NOT WIN32 AND (NOT APPLE OR ENABLE_SDL_ON_MACOS)) +if(SDL2_FOUND AND NOT SDL3_FOUND AND NOT WIN32) set(ZWIDGET_SOURCES ${ZWIDGET_SOURCES} ${ZWIDGET_SDL2_SOURCES}) set(ZWIDGET_DEFINES ${ZWIDGET_DEFINES} -DUSE_SDL2) endif() @@ -380,7 +378,7 @@ if(APPLE) else() target_link_libraries(zwidget PRIVATE ${ZWIDGET_LIBS}) endif() -if(SDL3_FOUND AND (NOT APPLE OR ENABLE_SDL_ON_MACOS)) +if(SDL3_FOUND) target_include_directories(zwidget PRIVATE ${SDL3_INCLUDE_DIRS}) if(TARGET SDL3::SDL3) target_link_libraries(zwidget PRIVATE SDL3::SDL3) @@ -388,7 +386,7 @@ if(SDL3_FOUND AND (NOT APPLE OR ENABLE_SDL_ON_MACOS)) target_link_libraries(zwidget PRIVATE ${SDL3_LIBRARY}) endif() endif() -if(SDL2_FOUND AND NOT SDL3_FOUND AND (NOT APPLE OR ENABLE_SDL_ON_MACOS)) +if(SDL2_FOUND AND NOT SDL3_FOUND) target_include_directories(zwidget PRIVATE ${SDL2_INCLUDE_DIRS}) if(TARGET SDL2::SDL2) target_link_libraries(zwidget PRIVATE SDL2::SDL2) @@ -396,7 +394,7 @@ if(SDL2_FOUND AND NOT SDL3_FOUND AND (NOT APPLE OR ENABLE_SDL_ON_MACOS)) target_link_libraries(zwidget PRIVATE ${SDL2_LIBRARY}) endif() endif() -set_target_properties(zwidget PROPERTIES CXX_STANDARD 17) +set_target_properties(zwidget PROPERTIES CXX_STANDARD 20) target_compile_options(zwidget PRIVATE ${CXX_WARNING_FLAGS}) if(MSVC) @@ -439,15 +437,15 @@ if(ZWIDGET_BUILD_EXAMPLE) target_link_libraries(zwidget_example PRIVATE ${ZWIDGET_LIBS}) endif() - if(SDL3_FOUND AND (NOT APPLE OR ENABLE_SDL_ON_MACOS)) + if(SDL3_FOUND) target_link_libraries(zwidget_example PRIVATE SDL3::SDL3) endif() - if(SDL2_FOUND AND NOT SDL3_FOUND AND (NOT APPLE OR ENABLE_SDL_ON_MACOS)) + if(SDL2_FOUND AND NOT SDL3_FOUND) target_link_libraries(zwidget_example PRIVATE SDL2::SDL2) endif() - set_target_properties(zwidget_example PROPERTIES CXX_STANDARD 17) + set_target_properties(zwidget_example PROPERTIES CXX_STANDARD 20) if(MSVC) set_property(TARGET zwidget_example PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") diff --git a/include/zwidget/widgets/dropdown/dropdown.h b/include/zwidget/widgets/dropdown/dropdown.h index 1c44068..31e9a5d 100644 --- a/include/zwidget/widgets/dropdown/dropdown.h +++ b/include/zwidget/widgets/dropdown/dropdown.h @@ -20,7 +20,6 @@ class Dropdown : public Widget { public: Dropdown(Widget* parent); - ~Dropdown(); void AddItem(const std::string& text, int index = -1); bool UpdateItem(const std::string& text, int index); @@ -62,7 +61,6 @@ class Dropdown : public Widget bool dropdownOpen = false; Widget* dropdown = nullptr; DropdownList* listView = nullptr; - Widget* pendingDeleteDropdown = nullptr; int maxDisplayItems = 0; bool dropdownDirection = true; diff --git a/src/core/canvas.cpp b/src/core/canvas.cpp index f8f934d..d37b926 100644 --- a/src/core/canvas.cpp +++ b/src/core/canvas.cpp @@ -248,13 +248,9 @@ void Canvas::pushClip(const Rect& box) y1 = std::min(y1, clip.y + clip.height); if (x0 < x1 && y0 < y1) - { clipStack.push_back(Rect::ltrb(x0, y0, x1, y1)); - } else - { clipStack.push_back(Rect::xywh(0.0, 0.0, 0.0, 0.0)); - } } else { @@ -1097,7 +1093,6 @@ void BitmapCanvas::begin(const Colorf& color) uint32_t b = (int32_t)clamp(color.b * 255.0f, 0.0f, 255.0f); uint32_t a = (int32_t)clamp(color.a * 255.0f, 0.0f, 255.0f); uint32_t bgcolor = (a << 24) | (r << 16) | (g << 8) | b; - pixels.clear(); pixels.resize(width * height, bgcolor); } diff --git a/src/core/resourcedata_mac.mm b/src/core/resourcedata_mac.mm index ad9bd44..8fe39f0 100644 --- a/src/core/resourcedata_mac.mm +++ b/src/core/resourcedata_mac.mm @@ -24,49 +24,31 @@ CTFontRef ctFont = (__bridge CTFontRef)systemFont; CFURLRef fontURL = (CFURLRef)CTFontCopyAttribute(ctFont, kCTFontURLAttribute); + if (!fontURL) + throw std::runtime_error("Failed to get font URL from system font"); - std::string fontPath; + // __bridge_transfer transfers ownership to ARC, so no manual CFRelease is needed + NSString* fontPath = (NSString*)CFURLCopyFileSystemPath(fontURL, kCFURLPOSIXPathStyle); + if (!fontPath) + throw std::runtime_error("Failed to convert font URL to file path"); - if (fontURL) + // Read the font file data + try + { + fontData.fontdata = ReadAllBytes(std::string([fontPath UTF8String])); + } + catch (const std::exception& e) { - CFStringRef cfPath = CFURLCopyFileSystemPath(fontURL, kCFURLPOSIXPathStyle); - if (cfPath) - { - fontPath = std::string([(NSString*)cfPath UTF8String]); - CFRelease(cfPath); - } CFRelease(fontURL); + throw std::runtime_error(std::string("Error reading system font file: ") + e.what()); } - - // Fallback to known macOS font paths if system font URL lookup fails - // This handles newer macOS versions where system fonts might be embedded - if (fontPath.empty()) + catch (...) { - // Try common macOS system font locations - const char* fallbackPaths[] = { - "/System/Library/Fonts/SFNS.ttf", // San Francisco (newer macOS) - "/System/Library/Fonts/SFNSText.ttf", // San Francisco Text - "/System/Library/Fonts/Helvetica.ttc", // Helvetica - "/System/Library/Fonts/HelveticaNeue.ttc", // Helvetica Neue - "/System/Library/Fonts/LucidaGrande.ttc", // Lucida Grande (older macOS) - "/Library/Fonts/Arial.ttf", // Arial fallback - }; - - for (const char* path : fallbackPaths) - { - std::ifstream test(path); - if (test.good()) - { - fontPath = path; - break; - } - } + CFRelease(fontURL); + throw; } - if (fontPath.empty()) - throw std::runtime_error("Could not find system font"); - - fontData.fontdata = ReadAllBytes(fontPath); + CFRelease(fontURL); } return { fontData }; } @@ -76,59 +58,47 @@ SingleFontData fontData; @autoreleasepool { - NSFont* systemFont = nil; - if (@available(macOS 10.15, *)) { - systemFont = [NSFont monospacedSystemFontOfSize:13.0 weight:NSFontWeightRegular]; // Use a default size - } - if (!systemFont) { - // Fallback for older macOS versions - systemFont = [NSFont systemFontOfSize:13.0]; - } - if (!systemFont) // Double check after fallback - throw std::runtime_error("Failed to get system font"); - - CTFontRef ctFont = (__bridge CTFontRef)systemFont; CFURLRef fontURL = (CFURLRef)CTFontCopyAttribute(ctFont, kCTFontURLAttribute); - - std::string fontPath; - - if (fontURL) + NSFont* systemFont = nil; + if (@available(macOS 10.15, *)) { + systemFont = [NSFont monospacedSystemFontOfSize:13.0 weight:NSFontWeightRegular]; // Use a default size + } + + + if (!systemFont) { + // Fallback for older macOS versions + systemFont = [NSFont systemFontOfSize:13.0]; + } + + if (!systemFont) + // Double check after fallback + throw std::runtime_error("Failed to get system font"); + + CTFontRef ctFont = (__bridge CTFontRef)systemFont; + CFURLRef fontURL = (CFURLRef)CTFontCopyAttribute(ctFont, kCTFontURLAttribute); + if (!fontURL) + throw std::runtime_error("Failed to get font URL from system font"); + + NSString* fontPath = (NSString*)CFURLCopyFileSystemPath(fontURL, kCFURLPOSIXPathStyle); + if (!fontPath) + throw std::runtime_error("Failed to convert font URL to file path"); + + // Read the font file data + try + { + fontData.fontdata = ReadAllBytes(std::string([fontPath UTF8String])); + } + catch (const std::exception& e) { - CFStringRef cfPath = CFURLCopyFileSystemPath(fontURL, kCFURLPOSIXPathStyle); - if (cfPath) - { - fontPath = std::string([(NSString*)cfPath UTF8String]); - CFRelease(cfPath); - } CFRelease(fontURL); + throw std::runtime_error(std::string("Error reading system font file: ") + e.what()); } - - // Fallback to known macOS monospace font paths if system font URL lookup fails - if (fontPath.empty()) + catch (...) { - // Try common macOS monospace font locations - const char* fallbackPaths[] = { - "/System/Library/Fonts/SFNSMono.ttf", // SF Mono (newer macOS) - "/System/Library/Fonts/Monaco.ttf", // Monaco - "/Library/Fonts/Courier New.ttf", // Courier New - "/System/Library/Fonts/Courier.dfont", // Courier fallback - "/Library/Fonts/Menlo.ttc", // Menlo - }; - - for (const char* path : fallbackPaths) - { - std::ifstream test(path); - if (test.good()) - { - fontPath = path; - break; - } - } + CFRelease(fontURL); + throw; } - if (fontPath.empty()) - throw std::runtime_error("Could not find monospace system font"); - - fontData.fontdata = ReadAllBytes(fontPath); + CFRelease(fontURL); } return { std::move(fontData) }; } diff --git a/src/core/resourcedata_unix.cpp b/src/core/resourcedata_unix.cpp index 817f1a7..6749335 100644 --- a/src/core/resourcedata_unix.cpp +++ b/src/core/resourcedata_unix.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include static std::vector ReadAllBytes(const std::string& filename) @@ -22,18 +23,24 @@ static std::vector ReadAllBytes(const std::string& filename) return buffer; } -// Use fontconfig directly instead of GTK to find system fonts -// This works on all Linux desktop environments (GNOME, KDE, XFCE, etc.) -// and doesn't require GTK/GNOME desktop settings -static std::vector GetSystemFont(const char* fcName) +static std::vector GetGtkUIFont(const std::string& propertyName) { - std::string filename; + // Ask GTK what the UI font is: + + GSettings *settings = g_settings_new ("org.gnome.desktop.interface"); + gchar* str = g_settings_get_string(settings, propertyName.c_str()); + if (!str) + throw std::runtime_error("Could not get gtk font property"); + std::string fontname = str; + g_free(str); + + // Find the font filename using fontconfig: - // Use fontconfig to find the font + std::string filename; FcConfig* config = FcInitLoadConfigAndFonts(); if (config) { - FcPattern* pat = FcNameParse((const FcChar8*)fcName); + FcPattern* pat = FcNameParse((const FcChar8*)(fontname.c_str())); if (pat) { FcConfigSubstitute(config, pat, FcMatchPattern); @@ -49,20 +56,9 @@ static std::vector GetSystemFont(const char* fcName) } FcPatternDestroy(pat); } - FcConfigDestroy(config); - } - - // Fallback to DejaVu fonts if fontconfig fails - if (filename.empty()) - { - if (std::string(fcName) == "monospace") - filename = "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"; - else - filename = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"; } - if (filename.empty()) - throw std::runtime_error(std::string("Could not find font: ") + fcName); + throw std::runtime_error("Could not find font filename for: " + fontname); SingleFontData fontdata; fontdata.fontdata = ReadAllBytes(filename); @@ -71,12 +67,12 @@ static std::vector GetSystemFont(const char* fcName) std::vector ResourceData::LoadSystemFont() { - return GetSystemFont("sans-serif"); + return GetGtkUIFont("font-name"); } std::vector ResourceData::LoadMonospaceSystemFont() { - return GetSystemFont("monospace"); + return GetGtkUIFont("monospace-font-name"); } double ResourceData::GetSystemFontSize() diff --git a/src/core/truetypefont.cpp b/src/core/truetypefont.cpp index 4bbb377..bfc4b4d 100644 --- a/src/core/truetypefont.cpp +++ b/src/core/truetypefont.cpp @@ -89,6 +89,7 @@ TTCFontName TrueTypeFont::GetFontName() const TrueTypeTextMetrics TrueTypeFont::GetTextMetrics(double height) const { double scale = height / head.unitsPerEm; + double internalLeading = height - os2.sTypoAscender * scale + os2.sTypoDescender * scale; TrueTypeTextMetrics metrics; if (os2.usWinAscent != 0 || os2.usWinDescent != 0) @@ -1012,9 +1013,9 @@ void TTF_TableDirectory::Load(TrueTypeFileReader& reader) numTables = reader.ReadUInt16(); // opentype spec says we can't use these for security reasons, so we pretend they never was part of the header - reader.ReadUInt16(); // searchRange - reader.ReadUInt16(); // entrySelector - reader.ReadUInt16(); // rangeShift + ttf_uint16 searchRange = reader.ReadUInt16(); + ttf_uint16 entrySelector = reader.ReadUInt16(); + ttf_uint16 rangeShift = reader.ReadUInt16(); for (ttf_uint16 i = 0; i < numTables; i++) { @@ -2024,13 +2025,16 @@ TrueTypeGlyph TrueTypeFont::LoadCFFGlyph(uint32_t glyphIndex, double height) con double scaleY = -1.0; 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; } if (glyphIndex >= cff.CharStrings.size()) @@ -2052,6 +2056,7 @@ TrueTypeGlyph TrueTypeFont::LoadCFFGlyph(uint32_t glyphIndex, double height) con bool endchar = false; bool widthArg = true; + double widthValue = cff.PrivateDict.defaultWidthX; PathPoint cur, cp1, cp2, flex1start; while (!endchar) { @@ -2092,7 +2097,7 @@ TrueTypeGlyph TrueTypeFont::LoadCFFGlyph(uint32_t glyphIndex, double height) con if (oper == 12) oper = 1200 + (int)reader.ReadCard8(); - double tmp; + double tmp, fd; switch (oper) { // Path construction: @@ -2101,6 +2106,7 @@ TrueTypeGlyph TrueTypeFont::LoadCFFGlyph(uint32_t glyphIndex, double height) con { if (operands.size() > 2) { + widthValue = cff.PrivateDict.norminalWidthX + operands.get(0); operands.pop_front(); } widthArg = false; @@ -2116,6 +2122,7 @@ TrueTypeGlyph TrueTypeFont::LoadCFFGlyph(uint32_t glyphIndex, double height) con { if (operands.size() > 1) { + widthValue = cff.PrivateDict.norminalWidthX + operands.get(0); operands.pop_front(); } widthArg = false; @@ -2130,6 +2137,7 @@ TrueTypeGlyph TrueTypeFont::LoadCFFGlyph(uint32_t glyphIndex, double height) con { if (operands.size() > 1) { + widthValue = cff.PrivateDict.norminalWidthX + operands.get(0); operands.pop_front(); } widthArg = false; @@ -2453,7 +2461,7 @@ TrueTypeGlyph TrueTypeFont::LoadCFFGlyph(uint32_t glyphIndex, double height) con break; case 1235: // flex // To do: collapse to line when the flex depth is less than fd/100 device pixels - // fd = operands.get(12); // TODO: implement flex depth check + fd = operands.get(12); cur.x += operands.get(0); cur.y += operands.get(1); cp1 = cur; @@ -2548,6 +2556,7 @@ TrueTypeGlyph TrueTypeFont::LoadCFFGlyph(uint32_t glyphIndex, double height) con { if (operands.size() % 2 == 1) { + widthValue = cff.PrivateDict.norminalWidthX + operands.get(0); operands.pop_front(); } widthArg = false; @@ -2561,6 +2570,7 @@ TrueTypeGlyph TrueTypeFont::LoadCFFGlyph(uint32_t glyphIndex, double height) con { if (operands.size() % 2 == 1) { + widthValue = cff.PrivateDict.norminalWidthX + operands.get(0); operands.pop_front(); } widthArg = false; @@ -2581,6 +2591,7 @@ TrueTypeGlyph TrueTypeFont::LoadCFFGlyph(uint32_t glyphIndex, double height) con { if (operands.size() % 2 == 1) { + widthValue = cff.PrivateDict.norminalWidthX + operands.get(0); operands.pop_front(); } widthArg = false; @@ -2601,6 +2612,7 @@ TrueTypeGlyph TrueTypeFont::LoadCFFGlyph(uint32_t glyphIndex, double height) con { if (operands.size() % 2 == 1) { + widthValue = cff.PrivateDict.norminalWidthX + operands.get(0); operands.pop_front(); } widthArg = false; @@ -2621,6 +2633,7 @@ TrueTypeGlyph TrueTypeFont::LoadCFFGlyph(uint32_t glyphIndex, double height) con { if (operands.size() % 2 == 1) { + widthValue = cff.PrivateDict.norminalWidthX + operands.get(0); operands.pop_front(); } widthArg = false; diff --git a/src/core/truetypefont.h b/src/core/truetypefont.h index dda64dc..b3dcf42 100644 --- a/src/core/truetypefont.h +++ b/src/core/truetypefont.h @@ -15,8 +15,6 @@ class TTFDataBuffer static std::shared_ptr create(size_t size); static std::shared_ptr create(std::vector buffer); - virtual ~TTFDataBuffer() = default; - virtual char* data() = 0; virtual const char* data() const = 0; virtual size_t size() const = 0; diff --git a/src/core/widget.cpp b/src/core/widget.cpp index 2ade4a2..cad57e0 100644 --- a/src/core/widget.cpp +++ b/src/core/widget.cpp @@ -6,7 +6,6 @@ #include #include #include -#include Widget::Widget(Widget* parent, WidgetType type, RenderAPI renderAPI) : Type(type) { @@ -35,13 +34,6 @@ Widget::Widget(Widget* parent, WidgetType type, RenderAPI renderAPI) : Type(type Widget::~Widget() { - // Release cursor lock if this widget has it - Widget* window = Window(); - if (window && window->CursorLockWidget == this) - { - window->CursorLockWidget = nullptr; - } - for (auto subscription: Subscriptions) subscription->Subscribers.erase(this); @@ -406,10 +398,7 @@ void Widget::Update() void Widget::Repaint() { Widget* w = Window(); - if (!w) - return; - - if (!w->DispCanvas) + if (!w || !w->DispCanvas) return; Canvas* canvas = w->DispCanvas.get(); @@ -433,9 +422,7 @@ void Widget::Paint(Canvas* canvas) for (Widget* w = FirstChild(); w != nullptr; w = w->NextSibling()) { if (w->Type == WidgetType::Child && !w->HiddenFlag) - { w->Paint(canvas); - } } canvas->setOrigin(oldOrigin); canvas->popClip(); @@ -693,19 +680,11 @@ Widget* Widget::ChildAt(const Point& pos) Point Widget::MapFrom(const Widget* parent, const Point& pos) const { Point p = pos; - int iterations = 0; for (const Widget* cur = this; cur != nullptr; cur = cur->Parent()) { - iterations++; if (cur == parent) - { return p; - } p -= cur->ContentGeometry.topLeft(); - if (iterations > 100) - { - throw std::runtime_error("MapFrom: infinite loop detected"); - } } throw std::runtime_error("MapFrom: not a parent of widget"); } @@ -812,37 +791,18 @@ void Widget::OnWindowMouseDown(const Point& pos, InputKey key) { if (CursorLockWidget) { - try - { - Point mappedPos = CursorLockWidget->MapFrom(this, pos); - CursorLockWidget->OnMouseDown(mappedPos, key); - } - catch (const std::exception& e) - { - // Silently handle MapFrom errors for cursor-locked widgets - } + CursorLockWidget->OnMouseDown(CursorLockWidget->MapFrom(this, pos), key); } else { Widget* widget = ChildAt(pos); - if (!widget) widget = this; - while (widget) { - try - { - Point mappedPos = widget->MapFrom(this, pos); - bool stopPropagation = widget->OnMouseDown(mappedPos, key); - - if (stopPropagation || widget == this) - break; - } - catch (const std::exception& e) - { + bool stopPropagation = widget->OnMouseDown(widget->MapFrom(this, pos), key); + if (stopPropagation || widget == this) break; - } widget = widget->Parent(); } } @@ -873,15 +833,7 @@ void Widget::OnWindowMouseUp(const Point& pos, InputKey key) { if (CursorLockWidget) { - try - { - Point mappedPos = CursorLockWidget->MapFrom(this, pos); - CursorLockWidget->OnMouseUp(mappedPos, key); - } - catch (const std::exception& e) - { - // Silently handle MapFrom errors for cursor-locked widgets - } + CursorLockWidget->OnMouseUp(CursorLockWidget->MapFrom(this, pos), key); } else { @@ -890,17 +842,9 @@ void Widget::OnWindowMouseUp(const Point& pos, InputKey key) widget = this; while (widget) { - try - { - Point mappedPos = widget->MapFrom(this, pos); - bool stopPropagation = widget->OnMouseUp(mappedPos, key); - if (stopPropagation || widget == this) - break; - } - catch (const std::exception& e) - { + bool stopPropagation = widget->OnMouseUp(widget->MapFrom(this, pos), key); + if (stopPropagation || widget == this) break; - } widget = widget->Parent(); } } diff --git a/src/widgets/dropdown/dropdown.cpp b/src/widgets/dropdown/dropdown.cpp index ba6980c..2a0e43b 100644 --- a/src/widgets/dropdown/dropdown.cpp +++ b/src/widgets/dropdown/dropdown.cpp @@ -13,13 +13,6 @@ Dropdown::Dropdown(Widget* parent) : Widget(parent) p->Subscribe(this); } -Dropdown::~Dropdown() -{ - // Clean up any pending dropdown deletion - delete pendingDeleteDropdown; - pendingDeleteDropdown = nullptr; -} - DropdownList::DropdownList(Widget* parent, Dropdown* owner) : ListView(parent), owner(owner) { } @@ -306,20 +299,12 @@ void Dropdown::OnLostFocus() bool Dropdown::OpenDropdown() { - if (dropdownOpen || items.empty()) - { - return false; - } - - // Clean up any pending dropdown deletion from previous close - delete pendingDeleteDropdown; - pendingDeleteDropdown = nullptr; + if (dropdownOpen || items.empty()) return false; dropdownOpen = true; dropdown = new Widget(Window()); listView = new DropdownList(dropdown, this); - for (const auto& item : items) { listView->AddItem(item); @@ -336,9 +321,10 @@ bool Dropdown::OpenDropdown() GetWidth() + GetNoncontentLeft() + GetNoncontentRight(), GetDisplayItems() * 20.0 + 20 ); - OnGeometryChanged(); + dropdown->Show(); + listView->ScrollToItem(selectedItem); return true; @@ -346,23 +332,12 @@ bool Dropdown::OpenDropdown() bool Dropdown::CloseDropdown() { - if (!dropdownOpen || !dropdown) - { - return false; - } + if (!dropdownOpen || !dropdown) return false; - // Hide the dropdown immediately and mark as closed - // This prevents it from receiving further mouse events - dropdown->SetVisible(false); - dropdownOpen = false; - - // Mark dropdown for deferred deletion - we'll delete it when: - // 1. The Dropdown itself is destroyed - // 2. A new dropdown is opened - // This avoids deleting widgets during event processing - pendingDeleteDropdown = dropdown; + dropdown->Close(); dropdown = nullptr; listView = nullptr; + dropdownOpen = false; Update(); @@ -373,10 +348,8 @@ void Dropdown::OnDropdownActivated() { if (listView) { - int selectedItem = listView->GetSelectedItem(); - SetSelectedItem(selectedItem); + SetSelectedItem(listView->GetSelectedItem()); } - CloseDropdown(); SetFocus(); } diff --git a/src/widgets/listview/listview.cpp b/src/widgets/listview/listview.cpp index b606863..bb74f58 100644 --- a/src/widgets/listview/listview.cpp +++ b/src/widgets/listview/listview.cpp @@ -98,9 +98,7 @@ void ListView::RemoveItem(int index) void ListView::Activate() { if (OnActivated) - { OnActivated(); - } } void ListView::SetSelectedItem(int index) @@ -109,11 +107,8 @@ void ListView::SetSelectedItem(int index) { selectedItem = index; Update(); - if (OnChanged) - { OnChanged(selectedItem); - } } } @@ -193,10 +188,7 @@ bool ListView::OnMouseDown(const Point& pos, InputKey key) if (key == InputKey::LeftMouse) { - double itemHeight = getItemHeight(); - double scrollPos = scrollbar->GetPosition(); - int index = (int)((pos.y - 5.0 + scrollPos) / itemHeight); - + int index = (int)((pos.y - 5.0 + scrollbar->GetPosition()) / getItemHeight()); if (index >= 0 && (size_t)index < items.size()) { ScrollToItem(index); diff --git a/src/widgets/tabwidget/tabwidget.cpp b/src/widgets/tabwidget/tabwidget.cpp index d6259a5..7f49c86 100644 --- a/src/widgets/tabwidget/tabwidget.cpp +++ b/src/widgets/tabwidget/tabwidget.cpp @@ -172,14 +172,10 @@ void TabBar::SetCurrentIndex(int pageIndex) if (CurrentIndex != pageIndex) { if (CurrentIndex != -1) - { Tabs[CurrentIndex]->SetCurrent(false); - } CurrentIndex = pageIndex; if (CurrentIndex != -1) - { Tabs[CurrentIndex]->SetCurrent(true); - } } } @@ -190,9 +186,7 @@ void TabBar::OnTabClicked(TabBarTab* tab) { SetCurrentIndex(pageIndex); if (OnCurrentChanged) - { OnCurrentChanged(); - } } } @@ -320,9 +314,7 @@ void TabBarTab::OnMouseMove(const Point& pos) bool TabBarTab::OnMouseDown(const Point& pos, InputKey key) { if (OnClick) - { OnClick(); - } return true; } diff --git a/src/widgets/textedit/textedit.cpp b/src/widgets/textedit/textedit.cpp index 9cd35f6..d25d56a 100644 --- a/src/widgets/textedit/textedit.cpp +++ b/src/widgets/textedit/textedit.cpp @@ -296,9 +296,7 @@ bool TextEdit::OnMouseDown(const Point& pos, InputKey key) { SetPointerCapture(); mouse_selecting = true; - cursor_pos = GetCharacterIndex(pos); - selection_start = cursor_pos; selection_length = 0; @@ -556,35 +554,17 @@ void TextEdit::OnKeyUp(InputKey key) void TextEdit::OnSetFocus() { if (!readonly) - { timer->Start(500); - } - if (select_all_on_focus_gain) - { SelectAll(); - } - ignore_mouse_events = true; - - // Safety check: ensure lines is not empty before accessing - if (!lines.empty()) - { - cursor_pos.y = lines.size() - 1; - cursor_pos.x = lines[cursor_pos.y].text.length(); - } - else - { - cursor_pos.x = 0; - cursor_pos.y = 0; - } + cursor_pos.y = lines.size() - 1; + cursor_pos.x = lines[cursor_pos.y].text.length(); Update(); if (FuncFocusGained) - { FuncFocusGained(); - } } void TextEdit::OnLostFocus() @@ -927,12 +907,6 @@ bool TextEdit::InputMaskAcceptsInput(ivec2 cursor_pos, const std::string& str) std::string::size_type TextEdit::ToOffset(ivec2 pos) const { - if (lines.empty()) - return 0; - - if (pos.y < 0) - return 0; - if (pos.y < (int)lines.size()) { std::string::size_type offset = 0; @@ -940,7 +914,7 @@ std::string::size_type TextEdit::ToOffset(ivec2 pos) const { offset += lines[line].text.size() + 1; } - return offset + std::min((size_t)std::max(0, pos.x), lines[pos.y].text.size()); + return offset + std::min((size_t)pos.x, lines[pos.y].text.size()); } else { @@ -949,15 +923,12 @@ std::string::size_type TextEdit::ToOffset(ivec2 pos) const { offset += lines[line].text.size() + 1; } - return offset > 0 ? offset - 1 : 0; + return offset - 1; } } TextEdit::ivec2 TextEdit::FromOffset(std::string::size_type offset) const { - if (lines.empty()) - return ivec2(0, 0); - int line_offset = 0; for (int line = 0; line < (int)lines.size(); line++) { @@ -1056,19 +1027,7 @@ void TextEdit::OnPaint(Canvas* canvas) TextEdit::ivec2 TextEdit::GetCharacterIndex(Point mouse_wincoords) { - // Safety check: handle empty lines - if (lines.empty()) - { - return ivec2(0, 0); - } - - // Safety check: handle null canvas Canvas* canvas = GetCanvas(); - if (!canvas) - { - return ivec2(0, 0); - } - for (size_t i = 0; i < lines.size(); i++) { Line& line = lines[i]; @@ -1088,6 +1047,5 @@ TextEdit::ivec2 TextEdit::GetCharacterIndex(Point mouse_wincoords) } } - // lines is guaranteed non-empty here due to check above return ivec2(lines.back().text.size(), lines.size() - 1); }