From d01503cca5d0ff5fa8f795a3e5c600f7e48e82ea Mon Sep 17 00:00:00 2001 From: shenjack <3695888@qq.com> Date: Thu, 29 Dec 2022 11:42:26 +0800 Subject: [PATCH] feat: update pyglet --- libs/pyglet/__init__.py | 2 + libs/pyglet/font/directwrite.py | 538 ++++++++++++++++++++----- libs/pyglet/gl/cocoa.py | 7 +- libs/pyglet/libs/win32/constants.py | 2 + libs/pyglet/math.py | 132 +++--- libs/pyglet/shapes.py | 165 ++++++-- libs/pyglet/text/document.py | 2 +- libs/pyglet/text/formats/structured.py | 6 +- libs/pyglet/text/layout.py | 36 ++ 9 files changed, 681 insertions(+), 209 deletions(-) diff --git a/libs/pyglet/__init__.py b/libs/pyglet/__init__.py index c62a7d8..a80fcda 100644 --- a/libs/pyglet/__init__.py +++ b/libs/pyglet/__init__.py @@ -163,6 +163,7 @@ options = { 'headless': False, 'headless_device': 0, 'win32_disable_shaping': False, + 'dw_legacy_naming': False } _option_types = { @@ -192,6 +193,7 @@ _option_types = { 'headless': bool, 'headless_device': int, 'win32_disable_shaping': bool, + 'dw_legacy_naming': bool } diff --git a/libs/pyglet/font/directwrite.py b/libs/pyglet/font/directwrite.py index 7a90205..f850d6e 100644 --- a/libs/pyglet/font/directwrite.py +++ b/libs/pyglet/font/directwrite.py @@ -1,22 +1,18 @@ import copy +import os +import pathlib +import platform +from ctypes import * +from typing import List, Optional, Tuple +import math import pyglet from pyglet.font import base -from pyglet.util import debug_print -from pyglet.image.codecs.wic import IWICBitmap, GUID_WICPixelFormat32bppBGR, WICDecoder, GUID_WICPixelFormat32bppBGRA, \ - GUID_WICPixelFormat32bppPBGRA - -from pyglet import image -import ctypes -import math -from pyglet.libs.win32 import com +from pyglet.image.codecs.wic import IWICBitmap, WICDecoder, GUID_WICPixelFormat32bppPBGRA from pyglet.libs.win32 import _kernel32 as kernel32 -from pyglet.libs.win32 import _ole32 as ole32 from pyglet.libs.win32.constants import * from pyglet.libs.win32.types import * -from ctypes import * -import os -import platform +from pyglet.util import debug_print try: dwrite = 'dwrite' @@ -33,7 +29,9 @@ except OSError as err: # Doesn't exist? Should stop import of library. pass -_debug_font = debug_print('debug_font') +_debug_font = pyglet.options['debug_font'] + +_debug_print = debug_print('debug_font') def DWRITE_MAKE_OPENTYPE_TAG(a, b, c, d): @@ -102,6 +100,7 @@ name_to_stretch = {"undefined": DWRITE_FONT_STRETCH_UNDEFINED, "semiexpanded": DWRITE_FONT_STRETCH_SEMI_EXPANDED, "expanded": DWRITE_FONT_STRETCH_EXPANDED, "extraexpanded": DWRITE_FONT_STRETCH_EXTRA_EXPANDED, + "narrow": DWRITE_FONT_STRETCH_CONDENSED, } DWRITE_GLYPH_IMAGE_FORMATS = c_int @@ -146,6 +145,34 @@ INT32 = c_int32 UINT32 = c_uint32 UINT64 = c_uint64 +DWRITE_INFORMATIONAL_STRING_ID = UINT32 + +DWRITE_INFORMATIONAL_STRING_NONE = 0 +DWRITE_INFORMATIONAL_STRING_COPYRIGHT_NOTICE = 1 +DWRITE_INFORMATIONAL_STRING_VERSION_STRINGS = 2 +DWRITE_INFORMATIONAL_STRING_TRADEMARK = 3 +DWRITE_INFORMATIONAL_STRING_MANUFACTURER = 4 +DWRITE_INFORMATIONAL_STRING_DESIGNER = 5 +DWRITE_INFORMATIONAL_STRING_DESIGNER_URL = 6 +DWRITE_INFORMATIONAL_STRING_DESCRIPTION = 7 +DWRITE_INFORMATIONAL_STRING_FONT_VENDOR_URL = 8 +DWRITE_INFORMATIONAL_STRING_LICENSE_DESCRIPTION = 9 +DWRITE_INFORMATIONAL_STRING_LICENSE_INFO_URL = 10 +DWRITE_INFORMATIONAL_STRING_WIN32_FAMILY_NAMES = 11 +DWRITE_INFORMATIONAL_STRING_WIN32_SUBFAMILY_NAMES = 12 +DWRITE_INFORMATIONAL_STRING_TYPOGRAPHIC_FAMILY_NAMES = 13 +DWRITE_INFORMATIONAL_STRING_TYPOGRAPHIC_SUBFAMILY_NAMES = 14 +DWRITE_INFORMATIONAL_STRING_SAMPLE_TEXT = 15 +DWRITE_INFORMATIONAL_STRING_FULL_NAME = 16 +DWRITE_INFORMATIONAL_STRING_POSTSCRIPT_NAME = 17 +DWRITE_INFORMATIONAL_STRING_POSTSCRIPT_CID_NAME = 18 +DWRITE_INFORMATIONAL_STRING_WEIGHT_STRETCH_STYLE_FAMILY_NAME = 19 +DWRITE_INFORMATIONAL_STRING_DESIGN_SCRIPT_LANGUAGE_TAG = 20 +DWRITE_INFORMATIONAL_STRING_SUPPORTED_SCRIPT_LANGUAGE_TAG = 21 +DWRITE_INFORMATIONAL_STRING_PREFERRED_FAMILY_NAMES = 22 +DWRITE_INFORMATIONAL_STRING_PREFERRED_SUBFAMILY_NAMES = 23 +DWRITE_INFORMATIONAL_STRING_WWS_FAMILY_NAME = 24 + class D2D_POINT_2F(Structure): _fields_ = ( @@ -236,12 +263,64 @@ class DWRITE_CLUSTER_METRICS(ctypes.Structure): ) +class IDWriteFontFileStream(com.IUnknown): + _methods_ = [ + ('ReadFileFragment', + com.STDMETHOD(c_void_p, POINTER(c_void_p), UINT64, UINT64, POINTER(c_void_p))), + ('ReleaseFileFragment', + com.STDMETHOD(c_void_p, c_void_p)), + ('GetFileSize', + com.STDMETHOD(c_void_p, POINTER(UINT64))), + ('GetLastWriteTime', + com.STDMETHOD(c_void_p, POINTER(UINT64))), + ] + + +class IDWriteFontFileLoader_LI(com.IUnknown): # Local implementation use only. + _methods_ = [ + ('CreateStreamFromKey', + com.STDMETHOD(c_void_p, c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileStream)))) + ] + + +class IDWriteFontFileLoader(com.pIUnknown): + _methods_ = [ + ('CreateStreamFromKey', + com.STDMETHOD(c_void_p, c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileStream)))) + ] + + +class IDWriteLocalFontFileLoader(IDWriteFontFileLoader, com.pIUnknown): + _methods_ = [ + ('GetFilePathLengthFromKey', + com.STDMETHOD(c_void_p, UINT32, POINTER(UINT32))), + ('GetFilePathFromKey', + com.STDMETHOD(c_void_p, UINT32, c_wchar_p, UINT32)), + ('GetLastWriteTimeFromKey', + com.STDMETHOD()) + ] + + +IID_IDWriteLocalFontFileLoader = com.GUID(0xb2d9f3ec, 0xc9fe, 0x4a11, 0xa2, 0xec, 0xd8, 0x62, 0x08, 0xf7, 0xc0, 0xa2) + + +class IDWriteFontFile(com.pIUnknown): + _methods_ = [ + ('GetReferenceKey', + com.STDMETHOD(POINTER(c_void_p), POINTER(UINT32))), + ('GetLoader', + com.STDMETHOD(POINTER(IDWriteFontFileLoader))), + ('Analyze', + com.STDMETHOD()), + ] + + class IDWriteFontFace(com.pIUnknown): _methods_ = [ ('GetType', com.STDMETHOD()), ('GetFiles', - com.STDMETHOD()), + com.STDMETHOD(POINTER(UINT32), POINTER(IDWriteFontFile))), ('GetIndex', com.STDMETHOD()), ('GetSimulations', @@ -582,9 +661,9 @@ class IDWriteFontList(com.pIUnknown): ('GetFontCollection', com.STDMETHOD()), ('GetFontCount', - com.STDMETHOD()), + com.METHOD(UINT32)), ('GetFont', - com.STDMETHOD()), + com.STDMETHOD(UINT32, c_void_p)), # IDWriteFont, use void because of forward ref. ] @@ -610,33 +689,22 @@ class IDWriteFontFamily1(IDWriteFontFamily, IDWriteFontList, com.pIUnknown): ] -class IDWriteFontFile(com.pIUnknown): - _methods_ = [ - ('GetReferenceKey', - com.STDMETHOD()), - ('GetLoader', - com.STDMETHOD()), - ('Analyze', - com.STDMETHOD()), - ] - - class IDWriteFont(com.pIUnknown): _methods_ = [ ('GetFontFamily', com.STDMETHOD(POINTER(IDWriteFontFamily))), ('GetWeight', - com.STDMETHOD()), + com.METHOD(DWRITE_FONT_WEIGHT)), ('GetStretch', - com.STDMETHOD()), + com.METHOD(DWRITE_FONT_STRETCH)), ('GetStyle', - com.STDMETHOD()), + com.METHOD(DWRITE_FONT_STYLE)), ('IsSymbolFont', - com.STDMETHOD()), + com.METHOD(BOOL)), ('GetFaceNames', com.STDMETHOD(POINTER(IDWriteLocalizedStrings))), ('GetInformationalStrings', - com.STDMETHOD()), + com.STDMETHOD(DWRITE_INFORMATIONAL_STRING_ID, POINTER(IDWriteLocalizedStrings), POINTER(BOOL))), ('GetSimulations', com.STDMETHOD()), ('GetMetrics', @@ -664,7 +732,7 @@ class IDWriteFont1(IDWriteFont, com.pIUnknown): class IDWriteFontCollection(com.pIUnknown): _methods_ = [ ('GetFontFamilyCount', - com.STDMETHOD()), + com.METHOD(UINT32)), ('GetFontFamily', com.STDMETHOD(UINT32, POINTER(IDWriteFontFamily))), ('FindFamilyName', @@ -690,6 +758,21 @@ DWRITE_TEXT_ALIGNMENT_CENTER = 3 DWRITE_TEXT_ALIGNMENT_JUSTIFIED = 4 +class IDWriteGdiInterop(com.pIUnknown): + _methods_ = [ + ('CreateFontFromLOGFONT', + com.STDMETHOD(POINTER(LOGFONTW), POINTER(IDWriteFont))), + ('ConvertFontToLOGFONT', + com.STDMETHOD()), + ('ConvertFontFaceToLOGFONT', + com.STDMETHOD()), + ('CreateFontFaceFromHdc', + com.STDMETHOD(HDC, POINTER(IDWriteFontFace))), + ('CreateBitmapRenderTarget', + com.STDMETHOD()) + ] + + class IDWriteTextFormat(com.pIUnknown): _methods_ = [ ('SetTextAlignment', @@ -884,19 +967,6 @@ class IDWriteFontCollectionLoader(com.IUnknown): ] -class IDWriteFontFileStream(com.IUnknown): - _methods_ = [ - ('ReadFileFragment', - com.STDMETHOD(c_void_p, POINTER(c_void_p), UINT64, UINT64, POINTER(c_void_p))), - ('ReleaseFileFragment', - com.STDMETHOD(c_void_p, c_void_p)), - ('GetFileSize', - com.STDMETHOD(c_void_p, POINTER(UINT64))), - ('GetLastWriteTime', - com.STDMETHOD(c_void_p, POINTER(UINT64))), - ] - - class MyFontFileStream(com.COMObject): _interfaces_ = [IDWriteFontFileStream] @@ -938,15 +1008,8 @@ class MyFontFileStream(com.COMObject): return 0x80004001 # E_NOTIMPL -class IDWriteFontFileLoader(com.IUnknown): - _methods_ = [ - ('CreateStreamFromKey', - com.STDMETHOD(c_void_p, c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileStream)))) - ] - - class LegacyFontFileLoader(com.COMObject): - _interfaces_ = [IDWriteFontFileLoader] + _interfaces_ = [IDWriteFontFileLoader_LI] def __init__(self): self._streams = {} @@ -1089,7 +1152,7 @@ class IDWriteFactory(com.pIUnknown): ('CreateFontFileReference', com.STDMETHOD(c_wchar_p, c_void_p, POINTER(IDWriteFontFile))), ('CreateCustomFontFileReference', - com.STDMETHOD(c_void_p, UINT32, POINTER(IDWriteFontFileLoader), POINTER(IDWriteFontFile))), + com.STDMETHOD(c_void_p, UINT32, POINTER(IDWriteFontFileLoader_LI), POINTER(IDWriteFontFile))), ('CreateFontFace', com.STDMETHOD()), ('CreateRenderingParams', @@ -1101,14 +1164,14 @@ class IDWriteFactory(com.pIUnknown): ('RegisterFontFileLoader', com.STDMETHOD(c_void_p)), # Ambigious as newer is a pIUnknown and legacy is IUnknown. ('UnregisterFontFileLoader', - com.STDMETHOD(POINTER(IDWriteFontFileLoader))), + com.STDMETHOD(POINTER(IDWriteFontFileLoader_LI))), ('CreateTextFormat', com.STDMETHOD(c_wchar_p, IDWriteFontCollection, DWRITE_FONT_WEIGHT, DWRITE_FONT_STYLE, DWRITE_FONT_STRETCH, FLOAT, c_wchar_p, POINTER(IDWriteTextFormat))), ('CreateTypography', com.STDMETHOD(POINTER(IDWriteTypography))), ('GetGdiInterop', - com.STDMETHOD()), + com.STDMETHOD(POINTER(IDWriteGdiInterop))), ('CreateTextLayout', com.STDMETHOD(c_wchar_p, UINT32, IDWriteTextFormat, FLOAT, FLOAT, POINTER(IDWriteTextLayout))), ('CreateGdiCompatibleTextLayout', @@ -1548,6 +1611,13 @@ if not wic_decoder: raise Exception("Cannot use DirectWrite without a WIC Decoder") +def get_system_locale() -> str: + """Retrieve the string representing the system locale.""" + local_name = create_unicode_buffer(LOCALE_NAME_MAX_LENGTH) + kernel32.GetUserDefaultLocaleName(local_name, LOCALE_NAME_MAX_LENGTH) + return local_name.value + + class DirectWriteGlyphRenderer(base.GlyphRenderer): antialias_mode = D2D1_TEXT_ANTIALIAS_MODE_DEFAULT draw_options = D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT if WINDOWS_8_1_OR_GREATER else D2D1_DRAW_TEXT_OPTIONS_NONE @@ -1782,6 +1852,11 @@ class DirectWriteGlyphRenderer(base.GlyphRenderer): # Negative LSB: we shift the offset, otherwise the glyph will be cut off. render_offset_x = glyph_lsb * self.font.font_scale_ratio + # Increase width by arbitrary amount to accommodate size of italic. + # No way to get actual size of italics outside of rendering to larger texture and checking pixels. + if self.font.italic: + render_width += (render_width // 2) + # Create new bitmap. # TODO: We can probably adjust bitmap/baseline to reduce the whitespace and save a lot of texture space. # Note: Floating point precision makes this a giant headache, will need to be solved for this approach. @@ -1912,6 +1987,7 @@ class Win32DirectWriteFont(base.Font): texture_internalformat = pyglet.gl.GL_RGBA def __init__(self, name, size, bold=False, italic=False, stretch=False, dpi=None, locale=None): + self._filename: Optional[str] = None self._advance_cache = {} # Stores glyph's by the indice and advance. super(Win32DirectWriteFont, self).__init__() @@ -1919,9 +1995,6 @@ class Win32DirectWriteFont(base.Font): if not name: name = self._default_name - self._font_index, self._collection = self.get_collection(name) - assert self._collection is not None, "Font: {} not found in loaded or system font collection.".format(name) - self._name = name self.bold = bold self.size = size @@ -1965,6 +2038,27 @@ class Win32DirectWriteFont(base.Font): else: self._stretch = DWRITE_FONT_STRETCH_NORMAL + self._font_index, self._collection = self.get_collection(name) + write_font = None + # If not font found, search all collections for legacy GDI naming. + if pyglet.options["dw_legacy_naming"]: + if self._font_index is None and self._collection is None: + write_font, self._collection = self.find_font_face(name, self._weight, self._style, self._stretch) + + assert self._collection is not None, f"Font: '{name}' not found in loaded or system font collection." + + if self._font_index is not None: + font_family = IDWriteFontFamily1() + self._collection.GetFontFamily(self._font_index, byref(font_family)) + + write_font = IDWriteFont() + font_family.GetFirstMatchingFont( + self._weight, + self._stretch, + self._style, + byref(write_font) + ) + # Create the text format this font will use permanently. # Could technically be recreated, but will keep to be inline with other font objects. self._text_format = IDWriteTextFormat() @@ -1979,18 +2073,6 @@ class Win32DirectWriteFont(base.Font): byref(self._text_format) ) - # All this work just to get a font face and its metrics! - font_family = IDWriteFontFamily1() - self._collection.GetFontFamily(self._font_index, byref(font_family)) - - write_font = IDWriteFont() - font_family.GetFirstMatchingFont( - self._weight, - self._stretch, - self._style, - byref(write_font) - ) - font_face = IDWriteFontFace() write_font.CreateFontFace(byref(font_face)) @@ -2013,7 +2095,54 @@ class Win32DirectWriteFont(base.Font): self._fallback = IDWriteFontFallback() self._write_factory.GetSystemFontFallback(byref(self._fallback)) else: - assert _debug_font("Windows 8.1+ is required for font fallback. Colored glyphs cannot be omitted.") + assert _debug_print("Windows 8.1+ is required for font fallback. Colored glyphs cannot be omitted.") + + @property + def filename(self): + """Returns a filename associated with the font face. + Note: Capable of returning more than 1 file in the future, but will do just one for now.""" + if self._filename is not None: + return self._filename + + file_ct = UINT32() + self.font_face.GetFiles(byref(file_ct), None) + + font_files = (IDWriteFontFile * file_ct.value)() + + self.font_face.GetFiles(byref(file_ct), font_files) + + self._filename = "Not Available" + + pff = font_files[0] + + key_data = c_void_p() + ff_key_size = UINT32() + + pff.GetReferenceKey(byref(key_data), byref(ff_key_size)) + + loader = IDWriteFontFileLoader() + pff.GetLoader(byref(loader)) + + try: + local_loader = IDWriteLocalFontFileLoader() + loader.QueryInterface(IID_IDWriteLocalFontFileLoader, byref(local_loader)) + except OSError: # E_NOTIMPL + loader.Release() + pff.Release() + return self._filename + + path_len = UINT32() + local_loader.GetFilePathLengthFromKey(key_data, ff_key_size, byref(path_len)) + + buffer = create_unicode_buffer(path_len.value + 1) + local_loader.GetFilePathFromKey(key_data, ff_key_size, buffer, len(buffer)) + + loader.Release() + local_loader.Release() + pff.Release() + + self._filename = pathlib.PureWindowsPath(buffer.value).as_posix() # Convert to forward slashes. + return self._filename @property def name(self): @@ -2176,7 +2305,7 @@ class Win32DirectWriteFont(base.Font): for idx in clusters: ct = formatted_clusters.count(idx) if ct > 1: - substitutions[idx] = ct-1 + substitutions[idx] = ct - 1 for i in range(actual_count): indice = indices[i] @@ -2258,7 +2387,7 @@ class Win32DirectWriteFont(base.Font): # Note: RegisterFontLoader takes a pointer. However, for legacy we implement our own callback interface. # Therefore we need to pass to the actual pointer directly. - cls._write_factory.RegisterFontFileLoader(cls._font_loader.pointers[IDWriteFontFileLoader]) + cls._write_factory.RegisterFontFileLoader(cls._font_loader.pointers[IDWriteFontFileLoader_LI]) cls._font_collection_loader = LegacyCollectionLoader(cls._write_factory, cls._font_loader) cls._write_factory.RegisterFontCollectionLoader(cls._font_collection_loader) @@ -2297,6 +2426,7 @@ class Win32DirectWriteFont(base.Font): cls._custom_collection = IDWriteFontCollection1() cls._write_factory.CreateFontCollectionFromFontSet(cls._font_set, byref(cls._custom_collection)) + else: cls._font_cache.append(data) @@ -2311,7 +2441,7 @@ class Win32DirectWriteFont(base.Font): cls._font_collection_loader = LegacyCollectionLoader(cls._write_factory, cls._font_loader) cls._write_factory.RegisterFontCollectionLoader(cls._font_collection_loader) - cls._write_factory.RegisterFontFileLoader(cls._font_loader.pointers[IDWriteFontFileLoader]) + cls._write_factory.RegisterFontFileLoader(cls._font_loader.pointers[IDWriteFontFileLoader_LI]) cls._font_collection_loader.AddFontData(cls._font_cache) @@ -2323,7 +2453,7 @@ class Win32DirectWriteFont(base.Font): byref(cls._custom_collection)) @classmethod - def get_collection(cls, font_name): + def get_collection(cls, font_name) -> Tuple[Optional[int], Optional[IDWriteFontCollection1]]: """Returns which collection this font belongs to (system or custom collection), as well as its index in the collection.""" if not cls._write_factory: @@ -2344,8 +2474,8 @@ class Win32DirectWriteFont(base.Font): # Check if font is in the system collection. # Do not cache these values permanently as system font collection can be updated during runtime. + sys_collection = IDWriteFontCollection() if not font_exists.value: - sys_collection = IDWriteFontCollection() cls._write_factory.GetSystemFontCollection(byref(sys_collection), 1) sys_collection.FindFamilyName(create_unicode_buffer(font_name), byref(font_index), @@ -2354,57 +2484,245 @@ class Win32DirectWriteFont(base.Font): if font_exists.value: return font_index.value, sys_collection - # Font does not exist in either custom or system. return None, None @classmethod - def have_font(cls, name): + def find_font_face(cls, font_name, bold, italic, stretch) -> Tuple[ + Optional[IDWriteFont], Optional[IDWriteFontCollection]]: + """This will search font collections for legacy RBIZ names. However, matching to bold, italic, stretch is + problematic in that there are many values. We parse the font name looking for matches to the name database, + and attempt to pick the closest match. + This will search all fonts on the system and custom loaded, and all of their font faces. Returns a collection + and IDWriteFont if successful. + """ + p_bold, p_italic, p_stretch = cls.parse_name(font_name, bold, italic, stretch) + + _debug_print(f"directwrite: '{font_name}' not found. Attempting legacy name lookup in all collections.") + collection_idx = cls.find_legacy_font(cls._custom_collection, font_name, p_bold, p_italic, p_stretch) + if collection_idx is not None: + return collection_idx, cls._custom_collection + + sys_collection = IDWriteFontCollection() + cls._write_factory.GetSystemFontCollection(byref(sys_collection), 1) + + collection_idx = cls.find_legacy_font(sys_collection, font_name, p_bold, p_italic, p_stretch) + if collection_idx is not None: + return collection_idx, sys_collection + + return None, None + + @classmethod + def have_font(cls, name: str): if cls.get_collection(name)[0] is not None: return True return False - @classmethod - def get_font_face(cls, name): - # Check custom collection. - collection = None - font_index = UINT() - font_exists = BOOL() + @staticmethod + def parse_name(font_name: str, weight: int, style: int, stretch: int): + """Attempt at parsing any special names in a font for legacy checks. Takes the first found.""" - # Check custom collection. - if cls._custom_collection: - cls._custom_collection.FindFamilyName(create_unicode_buffer(name), - byref(font_index), - byref(font_exists)) + font_name = font_name.lower() + split_name = font_name.split(' ') - collection = cls._custom_collection + found_weight = weight + found_style = style + found_stretch = stretch - if font_exists.value == 0: - sys_collection = IDWriteFontCollection() - cls._write_factory.GetSystemFontCollection(byref(sys_collection), 1) - sys_collection.FindFamilyName(create_unicode_buffer(name), - byref(font_index), - byref(font_exists)) + # Only search if name is split more than once. + if len(split_name) > 1: + for name, value in name_to_weight.items(): + if name in split_name: + found_weight = value + break - collection = sys_collection + for name, value in name_to_style.items(): + if name in split_name: + found_style = value + break - if font_exists: - font_family = IDWriteFontFamily() - collection.GetFontFamily(font_index, byref(font_family)) + for name, value in name_to_stretch.items(): + if name in split_name: + found_stretch = value + break - write_font = IDWriteFont() - font_family.GetFirstMatchingFont(DWRITE_FONT_WEIGHT_NORMAL, - DWRITE_FONT_STRETCH_NORMAL, - DWRITE_FONT_STYLE_NORMAL, - byref(write_font)) + return found_weight, found_style, found_stretch - font_face = IDWriteFontFace1() - write_font.CreateFontFace(byref(font_face)) + @staticmethod + def find_legacy_font(collection: IDWriteFontCollection, font_name: str, bold, italic, stretch, full_debug=False) -> \ + Optional[IDWriteFont]: + coll_count = collection.GetFontFamilyCount() - return font_face + assert _debug_print(f"directwrite: Found {coll_count} fonts in collection.") + + locale = get_system_locale() + + for i in range(coll_count): + family = IDWriteFontFamily() + collection.GetFontFamily(i, byref(family)) + + # Just check the first character in Family Names to reduce search time. Arial -> A's only. + family_name_str = IDWriteLocalizedStrings() + family.GetFamilyNames(byref(family_name_str)) + + family_names = Win32DirectWriteFont.unpack_localized_string(family_name_str, locale) + family_name = family_names[0] + + if family_name[0] != font_name[0]: + family.Release() + continue + + assert _debug_print(f"directwrite: Inspecting family name: {family_name}") + + # Fonts in the family. Full search to search all font faces, typically the first will be good enough to tell + ft_ct = family.GetFontCount() + + face_names = [] + matches = [] + for j in range(ft_ct): + temp_ft = IDWriteFont() + family.GetFont(j, byref(temp_ft)) + + if _debug_font and full_debug: + fc_str = IDWriteLocalizedStrings() + temp_ft.GetFaceNames(byref(fc_str)) + + strings = Win32DirectWriteFont.unpack_localized_string(fc_str, locale) + face_names.extend(strings) + + print(f"directwrite: Face names found: {strings}") + + # Check for GDI compatibility name + compat_names = IDWriteLocalizedStrings() + exists = BOOL() + temp_ft.GetInformationalStrings(DWRITE_INFORMATIONAL_STRING_WIN32_FAMILY_NAMES, + byref(compat_names), + byref(exists)) + + # Successful in finding GDI name. + match_found = False + if exists.value != 0: + for compat_name in Win32DirectWriteFont.unpack_localized_string(compat_names, locale): + if compat_name == font_name: + assert _debug_print( + f"Found legacy name '{font_name}' as '{family_name}' in font face '{j}' (collection id #{i}).") + + match_found = True + matches.append((temp_ft.GetWeight(), temp_ft.GetStyle(), temp_ft.GetStretch(), temp_ft)) + break + + # Release resource if not a match. + if not match_found: + temp_ft.Release() + + family.Release() + + # If we have matches, we've already parsed through the proper family. Now try to match. + if matches: + write_font = Win32DirectWriteFont.match_closest_font(matches, bold, italic, stretch) + + # Cleanup other matches not used. + for match in matches: + if match[3] != write_font: + match[3].Release() # Release all other matches. + + return write_font return None + @staticmethod + def match_closest_font(font_list: List[Tuple[int, int, int, IDWriteFont]], bold: int, italic: int, stretch: int) -> \ + Optional[IDWriteFont]: + """Match the closest font to the parameters specified. If a full match is not found, a secondary match will be + found based on similar features. This can probably be improved, but it is possible you could get a different + font style than expected.""" + closest = [] + for match in font_list: + (f_weight, f_style, f_stretch, writefont) = match + + # Found perfect match, no need for the rest. + if f_weight == bold and f_style == italic and f_stretch == stretch: + _debug_print( + f"directwrite: full match found. (bold: {f_weight}, italic: {f_style}, stretch: {f_stretch})") + return writefont + + prop_match = 0 + similar_match = 0 + # Look for a full match, otherwise look for close enough. + # For example, Arial Black only has Oblique, not Italic, but good enough if you want slanted text. + if f_weight == bold: + prop_match += 1 + elif bold != DWRITE_FONT_WEIGHT_NORMAL and f_weight != DWRITE_FONT_WEIGHT_NORMAL: + similar_match += 1 + + if f_style == italic: + prop_match += 1 + elif italic != DWRITE_FONT_STYLE_NORMAL and f_style != DWRITE_FONT_STYLE_NORMAL: + similar_match += 1 + + if stretch == f_stretch: + prop_match += 1 + elif stretch != DWRITE_FONT_STRETCH_NORMAL and f_stretch != DWRITE_FONT_STRETCH_NORMAL: + similar_match += 1 + + closest.append((prop_match, similar_match, *match)) + + # If we get here, no perfect match, sort by highest perfect match, to secondary matches. + closest.sort(key=lambda fts: (fts[0], fts[1]), reverse=True) + + if closest: + # Take the first match after sorting. + closest_match = closest[0] + _debug_print(f"directwrite: falling back to partial match. " + f"(bold: {closest_match[2]}, italic: {closest_match[3]}, stretch: {closest_match[4]})") + return closest_match[5] + + return None + + @staticmethod + def unpack_localized_string(local_string: IDWriteLocalizedStrings, locale: str) -> List[str]: + """Takes IDWriteLocalizedStrings and unpacks the strings inside of it into a list.""" + str_array_len = local_string.GetCount() + + strings = [] + for _ in range(str_array_len): + string_size = UINT32() + + idx = Win32DirectWriteFont.get_localized_index(local_string, locale) + + local_string.GetStringLength(idx, byref(string_size)) + + buffer_size = string_size.value + + buffer = create_unicode_buffer(buffer_size + 1) + + local_string.GetString(idx, buffer, len(buffer)) + + strings.append(buffer.value) + + local_string.Release() + + return strings + + @staticmethod + def get_localized_index(strings: IDWriteLocalizedStrings, locale: str): + idx = UINT32() + exists = BOOL() + + if locale: + strings.FindLocaleName(locale, byref(idx), byref(exists)) + + if not exists.value: + # fallback to english. + strings.FindLocaleName('en-us', byref(idx), byref(exists)) + + if not exists: + return 0 + + return idx.value + + return 0 + d2d_factory = ID2D1Factory() hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, IID_ID2D1Factory, None, byref(d2d_factory)) diff --git a/libs/pyglet/gl/cocoa.py b/libs/pyglet/gl/cocoa.py index 88025d1..ca0e10f 100644 --- a/libs/pyglet/gl/cocoa.py +++ b/libs/pyglet/gl/cocoa.py @@ -194,10 +194,9 @@ class CocoaConfig(Config): elif _os_x_version >= os_x_release['lion']: # check for opengl profile # This requires OS-X Lion (Darwin 11) or higher - version = ( - getattr(self, 'major_version', None) or 2, - getattr(self, 'minor_version', None) - ) + version = (getattr(self, 'major_version', None) or 3, + getattr(self, 'minor_version', None) or 3) + # tell os-x we want to request a profile attrs.append(cocoapy.NSOpenGLPFAOpenGLProfile) diff --git a/libs/pyglet/libs/win32/constants.py b/libs/pyglet/libs/win32/constants.py index c3e6fa1..5ead51d 100644 --- a/libs/pyglet/libs/win32/constants.py +++ b/libs/pyglet/libs/win32/constants.py @@ -5101,3 +5101,5 @@ DWM_BB_TRANSITIONONMAXIMIZED = 0x00000004 STREAM_SEEK_SET = 0 STREAM_SEEK_CUR = 1 STREAM_SEEK_END = 2 + +LOCALE_NAME_MAX_LENGTH = 85 diff --git a/libs/pyglet/math.py b/libs/pyglet/math.py index 206a80b..7f36f66 100644 --- a/libs/pyglet/math.py +++ b/libs/pyglet/math.py @@ -41,6 +41,9 @@ supported. Helper methods are included for rotating, scaling, and transforming. The :py:class:`~pyglet.matrix.Mat4` includes class methods for creating orthographic and perspective projection matrixes. +Matrices behave just like they do in GLSL: they are specified in column-major +order and multiply on the left of vectors, which are treated as columns. + :note: For performance, Matrixes subclass the `tuple` type. They are therefore immutable - all operations return a new object; the object is not updated in-place. @@ -78,9 +81,6 @@ class Vec2: yield self.x yield self.y - def __len__(self) -> int: - return 2 - @_typing.overload def __getitem__(self, item: int) -> float: ... @@ -92,6 +92,16 @@ class Vec2: def __getitem__(self, item): return (self.x, self.y)[item] + def __setitem__(self, key, value): + if type(key) is slice: + for i, attr in enumerate(['x', 'y'][key]): + setattr(self, attr, value[i]) + else: + setattr(self, ['x', 'y'][key], value) + + def __len__(self) -> int: + return 2 + def __add__(self, other: Vec2) -> Vec2: return Vec2(self.x + other.x, self.y + other.y) @@ -315,6 +325,13 @@ class Vec3: def __getitem__(self, item): return (self.x, self.y, self.z)[item] + def __setitem__(self, key, value): + if type(key) is slice: + for i, attr in enumerate(['x', 'y', 'z'][key]): + setattr(self, attr, value[i]) + else: + setattr(self, ['x', 'y', 'z'][key], value) + def __len__(self) -> int: return 3 @@ -520,6 +537,13 @@ class Vec4: def __getitem__(self, item): return (self.x, self.y, self.z, self.w)[item] + def __setitem__(self, key, value): + if type(key) is slice: + for i, attr in enumerate(['x', 'y', 'z', 'w'][key]): + setattr(self, attr, value[i]) + else: + setattr(self, ['x', 'y', 'z', 'w'][key], value) + def __len__(self) -> int: return 4 @@ -696,30 +720,30 @@ class Mat3(tuple): def __matmul__(self, other): if isinstance(other, Vec3): - # Columns: - c0 = self[0::3] - c1 = self[1::3] - c2 = self[2::3] - return Vec3(sum(map(_mul, c0, other)), - sum(map(_mul, c1, other)), - sum(map(_mul, c2, other))) + # Rows: + r0 = self[0::3] + r1 = self[1::3] + r2 = self[2::3] + return Vec3(sum(map(_mul, r0, other)), + sum(map(_mul, r1, other)), + sum(map(_mul, r2, other))) if not isinstance(other, Mat3): raise TypeError("Can only multiply with Mat3 or Vec3 types") # Rows: - r0 = self[0:3] - r1 = self[3:6] - r2 = self[6:9] + r0 = self[0::3] + r1 = self[1::3] + r2 = self[2::3] # Columns: - c0 = other[0::3] - c1 = other[1::3] - c2 = other[2::3] + c0 = other[0:3] + c1 = other[3:6] + c2 = other[6:9] - # Multiply and sum rows * colums: - return Mat3((sum(map(_mul, r0, c0)), sum(map(_mul, r0, c1)), sum(map(_mul, r0, c2)), - sum(map(_mul, r1, c0)), sum(map(_mul, r1, c1)), sum(map(_mul, r1, c2)), - sum(map(_mul, r2, c0)), sum(map(_mul, r2, c1)), sum(map(_mul, r2, c2)))) + # Multiply and sum rows * columns: + return Mat3((sum(map(_mul, c0, r0)), sum(map(_mul, c0, r1)), sum(map(_mul, c0, r2)), + sum(map(_mul, c1, r0)), sum(map(_mul, c1, r1)), sum(map(_mul, c1, r2)), + sum(map(_mul, c2, r0)), sum(map(_mul, c2, r1)), sum(map(_mul, c2, r2)))) def __repr__(self) -> str: return f"{self.__class__.__name__}{self[0:3]}\n {self[3:6]}\n {self[6:9]}" @@ -765,7 +789,11 @@ class Mat4(tuple): z_near: float, z_far: float ) -> Mat4T: - """Create a Mat4 orthographic projection matrix.""" + """Create a Mat4 orthographic projection matrix for use with OpenGL. + + This matrix doesn't actually perform the projection; it transforms the + space so that OpenGL's vertex processing performs it. + """ width = right - left height = top - bottom depth = z_far - z_near @@ -792,7 +820,10 @@ class Mat4(tuple): fov: float = 60 ) -> Mat4T: """ - Create a Mat4 perspective projection matrix. + Create a Mat4 perspective projection matrix for use with OpenGL. + + This matrix doesn't actually perform the projection; it transforms the + space so that OpenGL's vertex processing performs it. :Parameters: `aspect` : The aspect ratio as a `float` @@ -857,17 +888,6 @@ class Mat4(tuple): 0.0, 0.0, 1.0, 0.0, vector[0], vector[1], vector[2], 1.0)) - @classmethod - def look_at_direction(cls: type[Mat4T], direction: Vec3, up: Vec3) -> Mat4T: - vec_z = direction.normalize() - vec_x = direction.cross(up).normalize() - vec_y = direction.cross(vec_z).normalize() - - return cls((vec_x.x, vec_y.x, vec_z.x, 0.0, - vec_x.y, vec_y.y, vec_z.y, 0.0, - vec_x.z, vec_z.z, vec_z.z, 0.0, - 0.0, 0.0, 0.0, 1.0)) - @classmethod def look_at(cls: type[Mat4T], position: Vec3, target: Vec3, up: Vec3): f = (target - position).normalize() @@ -882,11 +902,11 @@ class Mat4(tuple): def row(self, index: int) -> tuple: """Get a specific row as a tuple.""" - return self[index * 4: index * 4 + 4] + return self[index::4] def column(self, index: int) -> tuple: """Get a specific column as a tuple.""" - return self[index::4] + return self[index * 4: index * 4 + 4] def rotate(self, angle: float, vector: Vec3) -> Mat4: """Get a rotation Matrix on x, y, or z axis.""" @@ -1012,34 +1032,34 @@ class Mat4(tuple): def __matmul__(self, other): if isinstance(other, Vec4): - # Columns: - c0 = self[0::4] - c1 = self[1::4] - c2 = self[2::4] - c3 = self[3::4] - return Vec4(sum(map(_mul, c0, other)), - sum(map(_mul, c1, other)), - sum(map(_mul, c2, other)), - sum(map(_mul, c3, other))) + # Rows: + r0 = self[0::4] + r1 = self[1::4] + r2 = self[2::4] + r3 = self[3::4] + return Vec4(sum(map(_mul, r0, other)), + sum(map(_mul, r1, other)), + sum(map(_mul, r2, other)), + sum(map(_mul, r3, other))) if not isinstance(other, Mat4): raise TypeError("Can only multiply with Mat4 or Vec4 types") # Rows: - r0 = self[0:4] - r1 = self[4:8] - r2 = self[8:12] - r3 = self[12:16] + r0 = self[0::4] + r1 = self[1::4] + r2 = self[2::4] + r3 = self[3::4] # Columns: - c0 = other[0::4] - c1 = other[1::4] - c2 = other[2::4] - c3 = other[3::4] + c0 = other[0:4] + c1 = other[4:8] + c2 = other[8:12] + c3 = other[12:16] # Multiply and sum rows * columns: - return Mat4((sum(map(_mul, r0, c0)), sum(map(_mul, r0, c1)), sum(map(_mul, r0, c2)), sum(map(_mul, r0, c3)), - sum(map(_mul, r1, c0)), sum(map(_mul, r1, c1)), sum(map(_mul, r1, c2)), sum(map(_mul, r1, c3)), - sum(map(_mul, r2, c0)), sum(map(_mul, r2, c1)), sum(map(_mul, r2, c2)), sum(map(_mul, r2, c3)), - sum(map(_mul, r3, c0)), sum(map(_mul, r3, c1)), sum(map(_mul, r3, c2)), sum(map(_mul, r3, c3)))) + return Mat4((sum(map(_mul, c0, r0)), sum(map(_mul, c0, r1)), sum(map(_mul, c0, r2)), sum(map(_mul, c0, r3)), + sum(map(_mul, c1, r0)), sum(map(_mul, c1, r1)), sum(map(_mul, c1, r2)), sum(map(_mul, c1, r3)), + sum(map(_mul, c2, r0)), sum(map(_mul, c2, r1)), sum(map(_mul, c2, r2)), sum(map(_mul, c2, r3)), + sum(map(_mul, c3, r0)), sum(map(_mul, c3, r1)), sum(map(_mul, c3, r2)), sum(map(_mul, c3, r3)))) # def __getitem__(self, item): # row = [slice(0, 4), slice(4, 8), slice(8, 12), slice(12, 16)][item] diff --git a/libs/pyglet/shapes.py b/libs/pyglet/shapes.py index c377dc1..9767c1b 100644 --- a/libs/pyglet/shapes.py +++ b/libs/pyglet/shapes.py @@ -215,6 +215,7 @@ class ShapeBase(ABC): _group = None _num_verts = 0 _vertex_list = None + _draw_mode = GL_TRIANGLES def __del__(self): if self._vertex_list is not None: @@ -233,6 +234,21 @@ class ShapeBase(ABC): def _update_translation(self): self._vertex_list.translation[:] = (self._x, self._y) * self._num_verts + def _create_vertex_list(self): + """Build internal vertex list. + + This method must create a vertex list and assign it to + `self._vertex_list`. It is advisable to use it + during `__init__` and to then update the vertices accordingly + with `self._update_vertices`. + + While it is not mandatory to implement it, some properties ( + namely `batch` and `group`) rely on this method to properly + recreate the vertex list. + """ + raise NotImplementedError('_create_vertex_list must be defined in ' + 'order to use group or batch properties') + @abstractmethod def _update_vertices(self): """ @@ -253,7 +269,7 @@ class ShapeBase(ABC): shape to a `pyglet.graphics.Batch` for efficient rendering. """ self._group.set_state_recursive() - self._vertex_list.draw(GL_TRIANGLES) + self._vertex_list.draw(self._draw_mode) self._group.unset_state_recursive() def delete(self): @@ -404,8 +420,45 @@ class ShapeBase(ABC): self._visible = value self._update_vertices() + @property + def group(self): + """User assigned :class:`Group` object.""" + return self._group.parent + + @group.setter + def group(self, group): + if self._group.parent == group: + return + self._group = _ShapeGroup(self._group.blend_src, + self._group.blend_dest, + self._group.program, + group) + self._batch.migrate(self._vertex_list, self._draw_mode, self._group, + self._batch) + + @property + def batch(self): + """User assigned :class:`Batch` object.""" + return self._batch + + @batch.setter + def batch(self, batch): + if self._batch == batch: + return + + if batch is not None and self._batch is not None: + self._batch.migrate(self._vertex_list, self._draw_mode, + self._group, batch) + self._batch = batch + else: + self._vertex_list.delete() + self._batch = batch + self._create_vertex_list() + class Arc(ShapeBase): + _draw_mode = GL_LINES + def __init__(self, x, y, radius, segments=None, angle=math.tau, start_angle=0, closed=False, color=(255, 255, 255, 255), batch=None, group=None): """Create an Arc. @@ -460,11 +513,15 @@ class Arc(ShapeBase): program = get_default_shader() self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group) - self._vertex_list = program.vertex_list(self._num_verts, GL_LINES, self._batch, self._group, - colors=('Bn', self._rgba * self._num_verts), - translation=('f', (x, y) * self._num_verts)) + self._create_vertex_list() self._update_vertices() + def _create_vertex_list(self): + self._vertex_list = self._group.program.vertex_list( + self._num_verts, self._draw_mode, self._batch, self._group, + colors=('Bn', self._rgba * self._num_verts), + translation=('f', (self._x, self._y) * self._num_verts)) + def _update_vertices(self): if not self._visible: vertices = (0,) * (self._segments + 1) * 4 @@ -539,7 +596,7 @@ class Arc(ShapeBase): Using this method is not recommended. Instead, add the shape to a `pyglet.graphics.Batch` for efficient rendering. """ - self._vertex_list.draw(GL_LINES) + self._vertex_list.draw(self._draw_mode) class Circle(ShapeBase): @@ -582,11 +639,15 @@ class Circle(ShapeBase): self._batch = batch or Batch() self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group) - self._vertex_list = program.vertex_list(self._segments*3, GL_TRIANGLES, self._batch, self._group, - colors=('Bn', self._rgba * self._num_verts), - translation=('f', (x, y) * self._num_verts)) + self._create_vertex_list() self._update_vertices() + def _create_vertex_list(self): + self._vertex_list = self._group.program.vertex_list( + self._segments*3, self._draw_mode, self._batch, self._group, + colors=('Bn', self._rgba * self._num_verts), + translation=('f', (self._x, self._y) * self._num_verts)) + def _update_vertices(self): if not self._visible: vertices = (0,) * self._segments * 6 @@ -623,6 +684,8 @@ class Circle(ShapeBase): class Ellipse(ShapeBase): + _draw_mode = GL_LINES + def __init__(self, x, y, a, b, color=(255, 255, 255, 255), batch=None, group=None): """Create an ellipse. @@ -665,11 +728,15 @@ class Ellipse(ShapeBase): self._batch = batch or Batch() self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group) - self._vertex_list = program.vertex_list(self._num_verts, GL_LINES, self._batch, self._group, - colors=('Bn', self._rgba * self._num_verts), - translation=('f', (x, y) * self._num_verts)) + self._create_vertex_list() self._update_vertices() + def _create_vertex_list(self): + self._vertex_list = self._group.program.vertex_list( + self._num_verts, self._draw_mode, self._batch, self._group, + colors=('Bn', self._rgba * self._num_verts), + translation=('f', (self._x, self._y) * self._num_verts)) + def _update_vertices(self): if not self._visible: vertices = (0,) * self._num_verts * 4 @@ -738,7 +805,7 @@ class Ellipse(ShapeBase): Using this method is not recommended. Instead, add the shape to a `pyglet.graphics.Batch` for efficient rendering. """ - self._vertex_list.draw(GL_LINES) + self._vertex_list.draw(self._draw_mode) class Sector(ShapeBase): @@ -791,11 +858,15 @@ class Sector(ShapeBase): self._batch = batch or Batch() self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group) - self._vertex_list = program.vertex_list(self._num_verts, GL_TRIANGLES, self._batch, self._group, - colors=('Bn', self._rgba * self._num_verts), - translation=('f', (x, y) * self._num_verts)) + self._create_vertex_list() self._update_vertices() + def _create_vertex_list(self): + self._vertex_list = self._group.program.vertex_list( + self._num_verts, self._draw_mode, self._batch, self._group, + colors=('Bn', self._rgba * self._num_verts), + translation=('f', (self._x, self._y) * self._num_verts)) + def _update_vertices(self): if not self._visible: vertices = (0,) * self._segments * 6 @@ -918,11 +989,15 @@ class Line(ShapeBase): self._batch = batch or Batch() self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group) - self._vertex_list = program.vertex_list(6, GL_TRIANGLES, self._batch, self._group, - colors=('Bn', self._rgba * self._num_verts), - translation=('f', (x, y) * self._num_verts)) + self._create_vertex_list() self._update_vertices() + def _create_vertex_list(self): + self._vertex_list = self._group.program.vertex_list( + 6, self._draw_mode, self._batch, self._group, + colors=('Bn', self._rgba * self._num_verts), + translation=('f', (self._x, self._y) * self._num_verts)) + def _update_vertices(self): if not self._visible: self._vertex_list.vertices[:] = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) @@ -1013,11 +1088,15 @@ class Rectangle(ShapeBase): self._batch = batch or Batch() self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group) - self._vertex_list = program.vertex_list(6, GL_TRIANGLES, self._batch, self._group, - colors=('Bn', self._rgba * self._num_verts), - translation=('f', (x, y) * self._num_verts)) + self._create_vertex_list() self._update_vertices() + def _create_vertex_list(self): + self._vertex_list = self._group.program.vertex_list( + 6, self._draw_mode, self._batch, self._group, + colors=('Bn', self._rgba * self._num_verts), + translation=('f', (self._x, self._y) * self._num_verts)) + def _update_vertices(self): if not self._visible: self._vertex_list.vertices[:] = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) @@ -1143,12 +1222,16 @@ class BorderedRectangle(ShapeBase): self._batch = batch or Batch() self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group) - indices = [0, 1, 2, 0, 2, 3, 0, 4, 3, 4, 7, 3, 0, 1, 5, 0, 5, 4, 1, 2, 5, 5, 2, 6, 6, 2, 3, 6, 3, 7] - self._vertex_list = program.vertex_list_indexed(8, GL_TRIANGLES, indices, self._batch, self._group, - colors=('Bn', self._rgba * 4 + self._border_rgba * 4), - translation=('f', (x, y) * self._num_verts)) + self._create_vertex_list() self._update_vertices() + def _create_vertex_list(self): + indices = [0, 1, 2, 0, 2, 3, 0, 4, 3, 4, 7, 3, 0, 1, 5, 0, 5, 4, 1, 2, 5, 5, 2, 6, 6, 2, 3, 6, 3, 7] + self._vertex_list = self._group.program.vertex_list_indexed( + 8, self._draw_mode, indices, self._batch, self._group, + colors=('Bn', self._rgba * 4 + self._border_rgba * 4), + translation=('f', (self._x, self._y) * self._num_verts)) + def _update_color(self): self._vertex_list.colors[:] = self._rgba * 4 + self._border_rgba * 4 @@ -1318,11 +1401,15 @@ class Triangle(ShapeBase): self._batch = batch or Batch() self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group) - self._vertex_list = program.vertex_list(3, GL_TRIANGLES, self._batch, self._group, - colors=('Bn', self._rgba * self._num_verts), - translation=('f', (x, y) * self._num_verts)) + self._create_vertex_list() self._update_vertices() + def _create_vertex_list(self): + self._vertex_list = self._group.program.vertex_list( + 3, self._draw_mode, self._batch, self._group, + colors=('Bn', self._rgba * self._num_verts), + translation=('f', (self._x, self._y) * self._num_verts)) + def _update_vertices(self): if not self._visible: self._vertex_list.vertices[:] = (0, 0, 0, 0, 0, 0) @@ -1434,12 +1521,16 @@ class Star(ShapeBase): self._batch = batch or Batch() self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group) - self._vertex_list = program.vertex_list(self._num_verts, GL_TRIANGLES, self._batch, self._group, - colors=('Bn', self._rgba * self._num_verts), - rotation=('f', (rotation,) * self._num_verts), - translation=('f', (x, y) * self._num_verts)) + self._create_vertex_list() self._update_vertices() + def _create_vertex_list(self): + self._vertex_list = self._group.program.vertex_list( + self._num_verts, self._draw_mode, self._batch, self._group, + colors=('Bn', self._rgba * self._num_verts), + rotation=('f', (self._rotation,) * self._num_verts), + translation=('f', (self._x, self._y) * self._num_verts)) + def _update_vertices(self): if not self._visible: vertices = (0, 0) * self._num_spikes * 6 @@ -1541,12 +1632,16 @@ class Polygon(ShapeBase): self._batch = batch or Batch() self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group) - self._vertex_list = program.vertex_list(self._num_verts, GL_TRIANGLES, self._batch, self._group, - colors=('Bn', self._rgba * self._num_verts), - translation=('f', (coordinates[0]) * self._num_verts)) + self._create_vertex_list() self._update_vertices() self._update_color() + def _create_vertex_list(self): + self._vertex_list = self._group.program.vertex_list( + self._num_verts, self._draw_mode, self._batch, self._group, + colors=('Bn', self._rgba * self._num_verts), + translation=('f', (self._coordinates[0]) * self._num_verts)) + def _update_vertices(self): if not self._visible: self._vertex_list.vertices[:] = tuple([0] * ((len(self._coordinates) - 2) * 6)) diff --git a/libs/pyglet/text/document.py b/libs/pyglet/text/document.py index 23ec33c..1c40a31 100644 --- a/libs/pyglet/text/document.py +++ b/libs/pyglet/text/document.py @@ -217,7 +217,7 @@ class InlineElement: """ return self._position - def place(self, layout, x, y): + def place(self, layout, x, y, z): """Construct an instance of the element at the given coordinates. Called when the element's position within a layout changes, either diff --git a/libs/pyglet/text/formats/structured.py b/libs/pyglet/text/formats/structured.py index c6b31ea..8484ce4 100644 --- a/libs/pyglet/text/formats/structured.py +++ b/libs/pyglet/text/formats/structured.py @@ -87,8 +87,8 @@ class ImageElement(pyglet.text.document.InlineElement): descent = min(0, -anchor_y) super().__init__(ascent, descent, self.width) - def place(self, layout, x, y): - program = pyglet.text.layout.get_default_layout_shader() + def place(self, layout, x, y, z): + program = pyglet.text.layout.get_default_image_layout_shader() group = _InlineElementGroup(self.image.get_texture(), program, 0, layout.group) x1 = x y1 = y + self.descent @@ -96,7 +96,7 @@ class ImageElement(pyglet.text.document.InlineElement): y2 = y + self.height + self.descent vertex_list = program.vertex_list_indexed(4, pyglet.gl.GL_TRIANGLES, [0, 1, 2, 0, 2, 3], layout.batch, group, - position=('f', (x1, y1, x2, y1, x2, y2, x1, y2)), + position=('f', (x1, y1, z, x2, y1, z, x2, y2, z, x1, y2, z)), tex_coords=('f', self.image.tex_coords)) self.vertex_lists[layout] = vertex_list diff --git a/libs/pyglet/text/layout.py b/libs/pyglet/text/layout.py index 30d7824..94a4068 100644 --- a/libs/pyglet/text/layout.py +++ b/libs/pyglet/text/layout.py @@ -602,6 +602,31 @@ layout_fragment_source = """#version 330 core } """ +layout_fragment_image_source = """#version 330 core + in vec4 text_colors; + in vec2 texture_coords; + in vec4 vert_position; + + uniform sampler2D image_texture; + + out vec4 final_colors; + + uniform sampler2D text; + uniform bool scissor; + uniform vec4 scissor_area; + + void main() + { + final_colors = texture(image_texture, texture_coords.xy); + if (scissor == true) { + if (vert_position.x < scissor_area[0]) discard; // left + if (vert_position.y < scissor_area[1]) discard; // bottom + if (vert_position.x > scissor_area[0] + scissor_area[2]) discard; // right + if (vert_position.y > scissor_area[1] + scissor_area[3]) discard; // top + } + } +""" + decoration_vertex_source = """#version 330 core in vec3 position; in vec4 colors; @@ -673,6 +698,17 @@ def get_default_layout_shader(): return pyglet.gl.current_context.pyglet_text_layout_shader +def get_default_image_layout_shader(): + try: + return pyglet.gl.current_context.pyglet_text_layout_image_shader + except AttributeError: + pyglet.gl.current_context.pyglet_text_layout_image_shader = shader.ShaderProgram( + shader.Shader(layout_vertex_source, 'vertex'), + shader.Shader(layout_fragment_image_source, 'fragment'), + ) + return pyglet.gl.current_context.pyglet_text_layout_image_shader + + def get_default_decoration_shader(): try: return pyglet.gl.current_context.pyglet_text_decoration_shader