diff --git a/Quik.FreeType/FT.cs b/Quik.FreeType/FT.cs new file mode 100644 index 0000000..4af977e --- /dev/null +++ b/Quik.FreeType/FT.cs @@ -0,0 +1,83 @@ +using System; +using System.Runtime.InteropServices; + +namespace Quik.FreeType +{ + public static class FT + { + private const string freetype2 = "freetype"; + + [DllImport(freetype2, EntryPoint = "FT_Init_FreeType")] + public static extern FTError InitFreeType(out FTLibrary library); + + [DllImport(freetype2, EntryPoint = "FT_Done_FreeType")] + public static extern FTError DoneFreeType(FTLibrary library); + + [DllImport(freetype2, EntryPoint = "FT_New_Face")] + public static extern FTError NewFace( + FTLibrary library, + [MarshalAs(UnmanagedType.LPStr)] string path, + long faceIndex, + out FTFace face); + + [DllImport(freetype2, EntryPoint = "FT_New_Memory_Face")] + public static extern FTError NewMemoryFace( + FTLibrary library, + IntPtr buffer, + long size, + long faceIndex, + out FTFace face); + + [DllImport(freetype2, EntryPoint = "FT_New_Memory_Face")] + public static extern FTError NewMemoryFace( + FTLibrary library, + [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] byte[] buffer, + long size, + long faceIndex, + out FTFace face); + + [DllImport(freetype2, EntryPoint = "FT_New_Memory_Face")] + public static extern FTError NewMemoryFace( + FTLibrary library, + [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] Span buffer, + long size, + long faceIndex, + out FTFace face); + + // public static extern FTError OpenFace(FTLibrary library, in FTOpenArgs args, long faceIndex, out FTFace face); + + // [DllImport(freetype2, EntryPoint = "FT_Attach_File")] + // public static extern FTError AttachFile(FTFace face, [MarshalAs(UnmanagedType.LPStr)] string filePathName); + + [DllImport(freetype2, EntryPoint = "FT_Set_Char_Size")] + public static extern FTError SetCharSize( + FTFace library, + long charWidth, + long charHeight, + uint horizontalResolution, + uint verticalResolution); + + + [DllImport(freetype2, EntryPoint = "FT_Get_Char_Index")] + public static extern uint GetCharIndex(FTFace face, ulong charCode); + + [DllImport(freetype2, EntryPoint = "FT_Load_Glyph")] + public static extern FTError LoadGlyph(FTFace face, uint charIndex, FTLoadFlags flags); + + [DllImport(freetype2, EntryPoint = "FT_Render_Glyph")] + public static extern FTError RenderGlyph(FTGlyphSlot slot, FTRenderMode mode); + + [DllImport(freetype2, EntryPoint = "FT_Done_Face")] + public static extern FTError DoneFace(FTFace face); + + [DllImport(freetype2, EntryPoint = "FT_Bitmap_Init")] + public static extern void BitmapInit(ref FTBitmap bitmap); + + [DllImport(freetype2, EntryPoint = "FT_Bitmap_Convert")] + public static extern void BitmapConvert(FTLibrary library, in FTBitmap source, ref FTBitmap target, + int alignment); + + [DllImport(freetype2, EntryPoint = "FT_Bitmap_Done")] + public static extern void BitmapDone(FTLibrary library, ref FTBitmap bitmap); + } +} \ No newline at end of file diff --git a/Quik.FreeType/FTError.cs b/Quik.FreeType/FTError.cs new file mode 100644 index 0000000..d80c4b7 --- /dev/null +++ b/Quik.FreeType/FTError.cs @@ -0,0 +1,7 @@ +namespace Quik.FreeType +{ + public enum FTError : int + { + None = 0, + } +} \ No newline at end of file diff --git a/Quik.FreeType/FTLoadFlags.cs b/Quik.FreeType/FTLoadFlags.cs new file mode 100644 index 0000000..ad8ff4a --- /dev/null +++ b/Quik.FreeType/FTLoadFlags.cs @@ -0,0 +1,28 @@ +using System; + +namespace Quik.FreeType +{ + [Flags] + public enum FTLoadFlags + { + Default = 0, + NoScale = 1 << 0, + NoHinting = 1 << 1, + Render = 1 << 2, + NoBitmap = 1 << 3, + VerticalLayout = 1 << 4, + ForceAutoHint = 1 << 5, + CropBitmap = 1 << 6, + Pedantic = 1 << 7, + IgnoreGlobalAdvanceWidth = 1 << 9, + NoRecurse = 1 << 10, + IgnoreTransform = 1 << 11, + Monochrome= 1 << 12, + LinearDesign = 1 << 13, + SbitsOnly = 1 << 14, + NoAutoHint = 1 << 15, + Color = 1 << 20, + ComputeMetrics = 1 << 21, + BitmapMetricsOnly = 1 << 22 + } +} \ No newline at end of file diff --git a/Quik.FreeType/FTRenderMode.cs b/Quik.FreeType/FTRenderMode.cs new file mode 100644 index 0000000..c3cd212 --- /dev/null +++ b/Quik.FreeType/FTRenderMode.cs @@ -0,0 +1,12 @@ +namespace Quik.FreeType +{ + public enum FTRenderMode + { + Normal = 0, + Light, + Mono, + Lcd, + LcdVertical, + Sdf + } +} \ No newline at end of file diff --git a/Quik.FreeType/FaceFlag.cs b/Quik.FreeType/FaceFlag.cs new file mode 100644 index 0000000..dde662f --- /dev/null +++ b/Quik.FreeType/FaceFlag.cs @@ -0,0 +1,28 @@ +using System; + +namespace Quik.FreeType +{ + [Flags] + public enum FaceFlag : int + { + Scalable = 1 << 0, + FixedSizes = 1 << 1, + FixedWidth = 1 << 2, + Sfnt = 1 << 3, + Horizontal = 1 << 4, + Vertical = 1 << 5, + Kerning = 1 << 6, + FastGlyphs = 1 << 7, + MultipleMasters = 1 << 8, + GlyphNames = 1 << 9, + ExternalStream = 1 << 10, + Hinter = 1 << 11, + CidKeyed = 1 << 12, + Tricky = 1 << 13, + Color = 1 << 14, + Variation = 1 << 15, + Svg = 1 << 16, + Sbix = 1 << 17, + SbixOverlay = 1 << 18 + } +} \ No newline at end of file diff --git a/Quik.FreeType/FreeTypeFont.cs b/Quik.FreeType/FreeTypeFont.cs new file mode 100644 index 0000000..d34de5b --- /dev/null +++ b/Quik.FreeType/FreeTypeFont.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Quik.Typography; + +namespace Quik.FreeType +{ + public class FreeTypeFont : QuikFont, IDisposable + { + + private FreeTypeFontManager _owner; + private FTLibrary _library; + private FTFace _face; + private List _textures = new List(); + private Dictionary _entries = new Dictionary(); + + public override float Ascender => _face.ScaledSize.Ascender / 64f; + public override float Descender => _face.ScaledSize.Descender / 64f; + + internal FreeTypeFont(FreeTypeFontManager owner, FileInfo file, QuikFontStyle style) + { + Style = style; + _owner = owner; + _library = owner._library; + + if (FT.NewFace(_library, file.FullName, 0, out _face) != FTError.None) + { + throw new Exception("Could not load font file."); + } + + FT.SetCharSize(_face, 0, (long)(style.Size * 64), 96, 96); + + _textures.Add(new Atlas(owner.Context.TextureManager, _library)); + } + + public override QuikFontStyle Style { get; } + + public override bool HasCharacter(int character) + { + return FT.GetCharIndex(_face, (ulong)character) != 0; + } + + public override void GetCharacter(int character, out IQuikTexture texture, out QuikGlyph glyph) + { + GlyphEntry entry; + + if (_entries.TryGetValue(character, out entry)) + { + texture = entry.Atlas.Texture; + glyph = entry.Metrics; + return; + } + + entry = new GlyphEntry(); + entry.Character = character; + + uint index = FT.GetCharIndex(_face, (ulong) character); + FT.LoadGlyph(_face, index, FTLoadFlags.Default); + FT.RenderGlyph(_face.Glyph, FTRenderMode.Normal); + + entry.Atlas = _textures[_textures.Count - 1]; + if (!entry.Atlas.CanFit(_face.Glyph)) + { + entry.Atlas = new Atlas(_owner.Context.TextureManager, _library); + _textures.Add(entry.Atlas); + } + entry.Atlas.AttachGlyph(_face.Glyph, out QuikRectangle uvs); + + entry.Metrics = new QuikGlyph( + character, + uvs, + new QuikVec2(_face.Glyph.Metrics.Width / 64f, _face.Glyph.Metrics.Height / 64f), + new QuikVec2(_face.Glyph.Metrics.HorizontalBearingX / 64f, _face.Glyph.Metrics.HorizontalBearingY / 64f), + new QuikVec2(_face.Glyph.Metrics.VerticalBearingX / 64f , _face.Glyph.Metrics.VerticalBearingY / 64f), + new QuikVec2(_face.Glyph.Metrics.HorizontalAdvance / 64f, _face.Glyph.Metrics.VerticalAdvance / 64f)); + + _entries[character] = entry; + + texture = entry.Atlas.Texture; + glyph = entry.Metrics; + } + + public bool IsDisposed { get; private set; } = false; + + private void Dispose(bool disposing) + { + if (IsDisposed) return; + + if (disposing) + { + foreach (Atlas atlas in _textures) + { + atlas.Dispose(); + } + } + + FT.DoneFace(_face); + IsDisposed = true; + } + + public void Dispose() => Dispose(true); + + private class Atlas : IDisposable + { + public IQuikTexture Texture; + + private QuikVec2 _pointer = new QuikVec2(); + private float _verticalAdvance = 0; + private FTLibrary _ft; + private FTBitmap _bitmap; + + public Atlas(IQuikTextureManager textureManager, FTLibrary ft) + { + Texture = textureManager.CreateTexture( + new QuikVec2(4096, 4096), + false, + QuikImageFormat.RgbaU8); + + FT.BitmapInit(ref _bitmap); + _ft = ft; + } + + public bool CanFit(in FTGlyphSlot slot) + { + // FIXME: the atlas will overflow. + return true; + } + + public void AttachGlyph(in FTGlyphSlot slot, out QuikRectangle UVs) + { + FT.BitmapConvert(_ft, slot.Bitmap, ref _bitmap, 1); + + QuikRectangle position = + new QuikRectangle( + _pointer + new QuikVec2(_bitmap.Width + 1, _bitmap.Rows + 1), + _pointer + new QuikVec2(1, 1)); + + Texture.SubImage( + _bitmap.Buffer, + QuikImageFormat.AlphaU8, + position, + 0, + 1); + + _pointer.X += _bitmap.Width + 2; + _verticalAdvance = Math.Max(_verticalAdvance, slot.Bitmap.Rows + 2); + + UVs = new QuikRectangle( + position.Right / 4096, + position.Bottom / 4096, + position.Left / 4096, + position.Top / 4096 + ); + } + + private bool _isDisposed = false; + + public void Dispose(bool disposing) + { + if (_isDisposed) return; + if (disposing) + { + Texture.Dispose(); + } + FT.BitmapDone(_ft, ref _bitmap); + _isDisposed = true; + } + + public void Dispose() + { + } + } + + private class GlyphEntry + { + public int Character; + public Atlas Atlas; + public QuikGlyph Metrics; + } + } +} \ No newline at end of file diff --git a/Quik.FreeType/FreeTypeFontManager.cs b/Quik.FreeType/FreeTypeFontManager.cs new file mode 100644 index 0000000..b585e59 --- /dev/null +++ b/Quik.FreeType/FreeTypeFontManager.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Quik.Typography; + +namespace Quik.FreeType +{ + public class FreeTypeFontManager : IQuikFontManager, IDisposable + { + public QuikContext Context { get; set; } + + internal FTLibrary _library; + private Dictionary _cache = new Dictionary(); + private Dictionary _fonts= new Dictionary(); + + public event FreeTypeFontManagerFontFinder FontNotFound; + + public FreeTypeFontManager() + { + FT.InitFreeType(out _library); + + // FIXME: There are operating system specific ways to achieve this. This is + // definitely not the best way to do this. + + // Scan the fonts folder and build up a font cache. + string path = Environment.GetFolderPath(Environment.SpecialFolder.Fonts); + if (string.IsNullOrEmpty(path)) + { + if (OperatingSystem.IsLinux()) + { + path = "/usr/share/fonts"; + } + else if (OperatingSystem.IsWindows()) + { + path = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.Windows), + "Fonts"); + } + // For macOS I don't know. Too bad. + } + + DirectoryInfo directory = new DirectoryInfo(path); + ScanDirectoryForFonts(directory); + } + + private void ScanDirectoryForFonts(DirectoryInfo directory) + { + foreach (FileSystemInfo node in directory.GetFileSystemInfos()) + { + if (node.Attributes.HasFlag(FileAttributes.Directory)) + { + ScanDirectoryForFonts(node as DirectoryInfo); + } + else + { + ScanFileForFonts(node as FileInfo); + } + } + } + + private void ScanFileForFonts(FileInfo file) + { + if (file is null) return; + + if (FT.NewFace(_library, file.FullName, 0, out FTFace face) == FTError.None) + { + FontCacheEntry entry; + string name = face.FamilyName; + string style = face.StyleName; + + if (name is null) + goto done_face; + + if (_cache.ContainsKey(name)) + { + entry = _cache[name]; + } + else + { + entry = new FontCacheEntry(); + entry.Family = name; + _cache[name] = entry; + } + + switch (style.ToLowerInvariant()) + { + case "regular": + entry.Regular = file; + break; + case "bold": + entry.Bold = file; + break; + case "italic": + entry.Italic = file; + break; + } + + done_face: + FT.DoneFace(face); + } + } + + public void Clear() + { + // Nothing to do. + } + + public QuikFont GetFont(QuikFontStyle fontStyle) + { + FreeTypeFont font; + + if (_fonts.TryGetValue(fontStyle, out font)) + return font; + + FileInfo file = FindFont(fontStyle); + if (file == null) + { + FontNotFound?.Invoke(fontStyle, ref file); + if (file == null) + { + throw new Exception("Could not find the font you are looking for."); + } + } + + font = new FreeTypeFont(this, file, fontStyle); + _fonts.Add(fontStyle, font); + return font; + } + + private FileInfo FindFont(QuikFontStyle fontStyle) + { + FontCacheEntry entry; + if (_cache.TryGetValue(fontStyle.Family, out entry)) + { + switch (fontStyle.Type) + { + case QuikFontType.Normal: + return entry.Regular; + case QuikFontType.Bold: + return entry.Bold; + case QuikFontType.Italic: + return entry.Italic; + } + } + + return null; + } + + + // IDisposable + private void ReleaseUnmanagedResources() + { + FT.DoneFreeType(_library); + } + + public void Dispose() + { + ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + ~FreeTypeFontManager() + { + ReleaseUnmanagedResources(); + } + + private class FontCacheEntry + { + public string Family; + public FileInfo Regular; + public FileInfo Bold; + public FileInfo Italic; + } + } + + public delegate void FreeTypeFontManagerFontFinder(QuikFontStyle style, ref FileInfo info); +} \ No newline at end of file diff --git a/Quik.FreeType/Quik.FreeType.csproj b/Quik.FreeType/Quik.FreeType.csproj new file mode 100644 index 0000000..a11ae84 --- /dev/null +++ b/Quik.FreeType/Quik.FreeType.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + 7.3 + true + + + + + + + diff --git a/Quik.FreeType/Structures.cs b/Quik.FreeType/Structures.cs new file mode 100644 index 0000000..046b38f --- /dev/null +++ b/Quik.FreeType/Structures.cs @@ -0,0 +1,180 @@ +using System; +using System.Runtime.InteropServices; + +namespace Quik.FreeType +{ + public struct FTLibrary + { + private IntPtr _handle; + public IntPtr Handle => _handle; + } + + public unsafe struct FTFace + { + private IntPtr _handle; + public IntPtr Handle => _handle; + private unsafe FTFaceInternal* Ptr => (FTFaceInternal*)_handle; + + public long NumberOfGlyphs => Ptr->NumberOfGlyphs; + public long FaceIndex => Ptr->FaceIndex; + public FaceFlag FaceFlags => (FaceFlag)Ptr->FaceFlags; + public long StyleFlags => Ptr->StyleFlags; + public string FamilyName => Marshal.PtrToStringUTF8(Ptr->FamilyName); + public string StyleName => Marshal.PtrToStringUTF8(Ptr->StyleName); + public int NumberOfFixedSizes => Ptr->NumberOfFixedSizes; + public int NumberOfCharMaps => Ptr->NumberOfCharMaps; + public FTGlyphSlot Glyph => Ptr->Glyph; + public short Ascender => Ptr->Ascender; + public short Descender => Ptr->Descender; + public ref readonly FTSizeMetrics ScaledSize => ref ((FTSize*)Ptr->Size)->Metrics; + } + + public struct FTBox + { + public long XMin; + public long YMin; + public long XMax; + public long YMax; + } + + internal struct FTFaceInternal + { + public long NumberOfFaces; + public long FaceIndex; + public long FaceFlags; + public long StyleFlags; + public long NumberOfGlyphs; + public IntPtr FamilyName; + public IntPtr StyleName; + public int NumberOfFixedSizes; + public IntPtr AvailableSizes; + public int NumberOfCharMaps; + public IntPtr Charmaps; + public FTGeneric Generic; + public FTBox BoundingBox; + public ushort UnitsPerEm; + public short Ascender; + public short Descender; + public short Height; + public short MaxAdvanceWidth; + public short MaxAdvanceHeight; + public short UnderlinePosition; + public short UnderlineThickness; + public FTGlyphSlot Glyph; + public IntPtr Size; + public IntPtr Charmap; + + // Rest of the struct is private to implementation. + } + + public struct FTGeneric + { + public IntPtr Data; + public IntPtr Finalizer; + } + + public struct FTVector + { + public long X; + public long Y; + } + + public struct FTBitmap + { + public uint Rows; + public uint Width; + public int Pitch; + public IntPtr Buffer; + public ushort NumberOfGrays; + public byte PixelMode; + public byte PaletteMode; + public IntPtr Palette; + } + + public struct FTOutline + { + public short NumberOfContours; + public short NumberOfPoints; + public IntPtr Points; + public IntPtr Tags; + public IntPtr Contours; + public int Flags; + } + + internal struct FTGlyphSlotInternal + { + public FTLibrary Library; + public FTFace Face; + public FTGlyphSlot Next; + public uint GlyphIndex; + public FTGeneric Generic; + public FTGlyphMetrics Metrics; + public long LinearHorizontalAdvance; + public long LinearVerticalAdvance; + public FTVector Advance; + public int Format; + public FTBitmap Bitmap; + public int BitmapLeft; + public int BitmapTop; + public FTOutline Outline; + public uint NumberOfSubGlyphs; + public IntPtr SubGlyphs; + public IntPtr ControlData; + public long ControlLength; + public long LsbDelta; + public long RsbDelta; + public IntPtr Other; + public IntPtr Internal; + } + + public unsafe struct FTGlyphSlot + { + private IntPtr _handle; + public IntPtr Handle => _handle; + private FTGlyphSlotInternal* Ptr => (FTGlyphSlotInternal*) _handle; + + public FTLibrary Library => Ptr->Library; + public FTFace Face => Ptr->Face; + public FTGlyphSlot Next => Ptr->Next; + public uint GlyphIndex => Ptr->GlyphIndex; + public ref readonly FTGlyphMetrics Metrics => ref Ptr->Metrics; + public long LinearHorizontalAdvance => Ptr->LinearHorizontalAdvance; + public long LinearVerticalAdvance => Ptr->LinearVerticalAdvance; + public FTVector Advance => Ptr->Advance; + public ref readonly FTBitmap Bitmap => ref Ptr->Bitmap; + public long BitmapLeft => Ptr->BitmapLeft; + public long BitmapTop => Ptr->BitmapTop; + } + + public struct FTGlyphMetrics + { + public long Width; + public long Height; + public long HorizontalBearingX; + public long HorizontalBearingY; + public long HorizontalAdvance; + public long VerticalBearingX; + public long VerticalBearingY; + public long VerticalAdvance; + } + + public struct FTSizeMetrics + { + public short Xppem; + public short Yppem; + public long XScale; + public long YScale; + public long Ascender; + public long Descender; + public long Height; + public long MaxAdvance; + } + + public struct FTSize + { + public IntPtr Face; + public FTGeneric Generic; + public FTSizeMetrics Metrics; + private IntPtr Privates; + } +} \ No newline at end of file diff --git a/Quik.OpenTK/GL30Driver.cs b/Quik.OpenTK/GL30Driver.cs new file mode 100644 index 0000000..0361c24 --- /dev/null +++ b/Quik.OpenTK/GL30Driver.cs @@ -0,0 +1,147 @@ +using System; +using System.IO; +using System.Reflection; +using OpenTK.Graphics.OpenGL4; + +namespace Quik.OpenTK +{ + public class GL30Driver : IDisposable + { + public GL30Driver() + { + Assembly asm = typeof(GL30Driver).Assembly; + + using (StreamReader vert = new StreamReader(asm.GetManifestResourceStream("Quik.OpenTK.glsl.glsl130.vert"))) + using (StreamReader frag = new StreamReader(asm.GetManifestResourceStream("Quik.OpenTK.glsl.glsl130.frag"))) + { + int vs; + int fs; + + vs = GL.CreateShader(ShaderType.VertexShader); + fs = GL.CreateShader(ShaderType.FragmentShader); + + _sp = GL.CreateProgram(); + + GL.ShaderSource(vs, vert.ReadToEnd()); + GL.ShaderSource(fs, frag.ReadToEnd()); + + GL.CompileShader(vs); + GL.CompileShader(fs); + + #if DEBUG + int status; + + GL.GetShader(vs, ShaderParameter.CompileStatus, out status); + if (status == 0) + { + throw new Exception(GL.GetShaderInfoLog(vs)); + } + + GL.GetShader(fs, ShaderParameter.CompileStatus, out status); + if (status == 0) + { + throw new Exception(GL.GetShaderInfoLog(fs)); + } + #endif + + GL.AttachShader(_sp, vs); + GL.AttachShader(_sp, fs); + + GL.LinkProgram(_sp); + + #if DEBUG + GL.GetProgram(_sp, GetProgramParameterName.LinkStatus, out status); + if (status == 0) + { + throw new Exception(GL.GetProgramInfoLog(_sp)); + } + #endif + + GL.DetachShader(_sp, vs); + GL.DetachShader(_sp, fs); + GL.DeleteShader(vs); + GL.DeleteShader(fs); + } + + LoadUniform(_nameM4View, out _locM4View); + LoadUniform(_nameM4Model, out _locM4Model); + LoadUniform(_nameIFlags, out _locIFlags); + LoadUniform(_nameFSdfThreshold, out _locFSdfThreshold); + LoadUniform(_nameFSdfAuxilliaryThreshold, out _locFSdfAuxilliaryThreshold); + LoadUniform(_nameV4SdfAuxilliaryColor, out _locV4SdfAuxilliaryColor); + LoadUniform(_nameX2Texture, out _locX2Texture); + LoadUniform(_nameV2TextureOffset, out _locV2TextureOffset); + + LoadAttribute(_nameV2Postion, out _locV2Position); + LoadAttribute(_nameV2Texture, out _locV2Texture); + LoadAttribute(_nameV4Color, out _locV4Color); + + void LoadUniform(string name, out int location) + { + location = GL.GetUniformLocation(_sp, name); + } + + void LoadAttribute(string name, out int location) + { + location = GL.GetAttribLocation(_sp, name); + } + } + + private readonly int _sp; + + private const string _nameM4View = "m4View"; + private readonly int _locM4View; + private const string _nameM4Model = "m4Model"; + private readonly int _locM4Model; + private const string _nameV2Postion = "v2Position"; + private readonly int _locV2Position; + private const string _nameV2Texture = "v2Texture"; + private readonly int _locV2Texture; + private const string _nameV4Color = "v4Color"; + private readonly int _locV4Color; + private const string _nameIFlags = "iFlags"; + private readonly int _locIFlags; + private const string _nameFSdfThreshold = "fSdfThreshold"; + private readonly int _locFSdfThreshold; + private const string _nameFSdfAuxilliaryThreshold = "fSdfAuxilliaryThreshold"; + private readonly int _locFSdfAuxilliaryThreshold; + private const string _nameV4SdfAuxilliaryColor = "v4SdfAuxilliaryColor"; + private readonly int _locV4SdfAuxilliaryColor; + private const string _nameX2Texture = "x2Texture"; + private readonly int _locX2Texture; + private const string _nameV2TextureOffset = "v2TextureOffset"; + private readonly int _locV2TextureOffset; + + [Flags] + private enum Flags : int + { + Texture = 1 << 0, + DiscardEnable = 1 << 1, + Sdf = 1 << 2, + SdfAuxEnable = 1 << 3, + } + + private bool _isDisposed = false; + private void Dispose(bool disposing) + { + if (_isDisposed) return; + + if (disposing) + { + GL.DeleteProgram(_sp); + } + else + { + throw new Exception("OpenGL resource is leaked. Dispose unreferenced OpenGL objects in the context thread."); + } + + _isDisposed = true; + } + + public void Dispose() => Dispose(true); + ~GL30Driver() + { + Dispose(false); + } + } +} \ No newline at end of file diff --git a/Quik.OpenTK/OpenGLTexture.cs b/Quik.OpenTK/OpenGLTexture.cs index 25feada..8712469 100644 --- a/Quik.OpenTK/OpenGLTexture.cs +++ b/Quik.OpenTK/OpenGLTexture.cs @@ -16,9 +16,9 @@ namespace Quik.OpenTK Height = (int)size.Y; TextureId = GL.GenTexture(); - + GL.BindTexture(TextureTarget.Texture2D, TextureId); - + GL.TexParameter( TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, @@ -62,16 +62,18 @@ namespace Quik.OpenTK public bool Mipmaps { get; } /// - public void Image(IntPtr data, QuikImageFormat format, QuikVec2 size, int level) + public void Image(IntPtr data, QuikImageFormat format, QuikVec2 size, int level, int alignment = 4) { GL.BindTexture(TextureTarget.Texture2D, TextureId); + GL.PixelStore(PixelStoreParameter.UnpackAlignment, alignment); GL.TexSubImage2D(TextureTarget.Texture2D, level, 0, 0, Width, Height, GetGlImageFormat(format), GetGlDataFormat(format), data); } /// - public void SubImage(IntPtr data, QuikImageFormat format, QuikRectangle location, int level) + public void SubImage(IntPtr data, QuikImageFormat format, QuikRectangle location, int level, int alignment = 4) { GL.BindTexture(TextureTarget.Texture2D, TextureId); + GL.PixelStore(PixelStoreParameter.UnpackAlignment, alignment); GL.TexSubImage2D( TextureTarget.Texture2D, level, @@ -101,6 +103,8 @@ namespace Quik.OpenTK return PixelFormat.Rgb; case QuikImageFormat.RgbaF: case QuikImageFormat.RgbaU8: return PixelFormat.Rgba; + case QuikImageFormat.AlphaF: case QuikImageFormat.AlphaU8: + return PixelFormat.Alpha; default: throw new ArgumentOutOfRangeException(); } @@ -113,11 +117,14 @@ namespace Quik.OpenTK case QuikImageFormat.RedF: case QuikImageFormat.RgbaF: case QuikImageFormat.RgbF: + case QuikImageFormat.AlphaF: return PixelType.Float; case QuikImageFormat.RedU8: case QuikImageFormat.RgbaU8: case QuikImageFormat.RgbU8: + case QuikImageFormat.AlphaU8: return PixelType.UnsignedByte; + default: throw new ArgumentOutOfRangeException(); } diff --git a/Quik.OpenTK/Quik.OpenTK.csproj b/Quik.OpenTK/Quik.OpenTK.csproj index 0f1462d..977354d 100644 --- a/Quik.OpenTK/Quik.OpenTK.csproj +++ b/Quik.OpenTK/Quik.OpenTK.csproj @@ -12,6 +12,7 @@ + diff --git a/Quik.OpenTK/glsl/glsl130.frag b/Quik.OpenTK/glsl/glsl130.frag new file mode 100644 index 0000000..4763770 --- /dev/null +++ b/Quik.OpenTK/glsl/glsl130.frag @@ -0,0 +1,61 @@ +#version 130 + +#define F_TEXTURE (1 << 0) +#define F_DISCARD_EN (1 << 1) +#define F_SDF (1 << 2) +#define F_SDF_AUX_EN (1 << 3) + +uniform int iFlags; +uniform float fSdfThreshold; +uniform float fSdfAuxilliaryThreshold; +uniform vec4 v4SdfAuxilliaryColor; +uniform sampler2D x2Texture; +uniform vec2 v2TextureOffset; + +in vec2 fv2Texture; +out vec4 fv4Color; + +vec4 v4Color() +{ + vec4 color = fv4Color; + + if ((iFlags & F_TEXTURE) != 0) + { + if ((iFlags & F_SDF) != 0) + { + float a = texture(x2Texture, fv2Texture + v2TextureOffset).a; + + if ((iFlags & F_SDF_AUX_EN) != 0) + { + color = + (a > fSdfThreshold) + ? color + : (a > fSdfAuxilliaryThreshold) + ? v4SdfAuxilliaryColor + : vec4(0, 0, 0, 0); + } + else if (a < fSdfThreshold) + { + color = vec4(0, 0, 0, 0); + } + } + else + { + color *= texture(x2Texture, fv2Texture + v2TextureOffset); + } + } + + return color; +} + +void main() +{ + vec4 color = v4Color(); + + if ((iFlags & F_DISCARD_EN) != 0 && color.a <= 0) + { + discard; + } + + fv4Color = color; +} diff --git a/Quik.OpenTK/glsl/glsl130.vert b/Quik.OpenTK/glsl/glsl130.vert new file mode 100644 index 0000000..d4e128d --- /dev/null +++ b/Quik.OpenTK/glsl/glsl130.vert @@ -0,0 +1,19 @@ +#version 130 + +uniform mat4 m4View; +uniform mat4 m4Model; + +in vec2 v2Position; +in vec2 v2Texture; +in vec4 v4Color; + +out vec2 fv2Texture; +out vec4 fv4Color; + +void main() +{ + fv4Color = v4Color; + fv2Texture = v2Texture; + + gl_Position = m4View * m4Model * vec4(v2Position.xy, 1, 1); +} diff --git a/Quik.sln b/Quik.sln index 90061af..c10197d 100644 --- a/Quik.sln +++ b/Quik.sln @@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Quik.OpenTK", "Quik.OpenTK\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuikTestApplication", "QuikTestApplication\QuikTestApplication.csproj", "{49AEF502-692A-48A4-8076-EF2228925280}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Quik.FreeType", "Quik.FreeType\Quik.FreeType.csproj", "{53B95098-F304-47E6-A08C-DAFA589F5BCF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -53,5 +55,17 @@ Global {49AEF502-692A-48A4-8076-EF2228925280}.Release|x64.Build.0 = Release|Any CPU {49AEF502-692A-48A4-8076-EF2228925280}.Release|x86.ActiveCfg = Release|Any CPU {49AEF502-692A-48A4-8076-EF2228925280}.Release|x86.Build.0 = Release|Any CPU + {53B95098-F304-47E6-A08C-DAFA589F5BCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {53B95098-F304-47E6-A08C-DAFA589F5BCF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {53B95098-F304-47E6-A08C-DAFA589F5BCF}.Debug|x64.ActiveCfg = Debug|Any CPU + {53B95098-F304-47E6-A08C-DAFA589F5BCF}.Debug|x64.Build.0 = Debug|Any CPU + {53B95098-F304-47E6-A08C-DAFA589F5BCF}.Debug|x86.ActiveCfg = Debug|Any CPU + {53B95098-F304-47E6-A08C-DAFA589F5BCF}.Debug|x86.Build.0 = Debug|Any CPU + {53B95098-F304-47E6-A08C-DAFA589F5BCF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {53B95098-F304-47E6-A08C-DAFA589F5BCF}.Release|Any CPU.Build.0 = Release|Any CPU + {53B95098-F304-47E6-A08C-DAFA589F5BCF}.Release|x64.ActiveCfg = Release|Any CPU + {53B95098-F304-47E6-A08C-DAFA589F5BCF}.Release|x64.Build.0 = Release|Any CPU + {53B95098-F304-47E6-A08C-DAFA589F5BCF}.Release|x86.ActiveCfg = Release|Any CPU + {53B95098-F304-47E6-A08C-DAFA589F5BCF}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/Quik/Controls/Button.cs b/Quik/Controls/Button.cs new file mode 100644 index 0000000..bb1b6bd --- /dev/null +++ b/Quik/Controls/Button.cs @@ -0,0 +1,105 @@ +using Quik.Typography; + +namespace Quik.Controls +{ + public class Button : Control + { + public string Text { get; set; } = "Button"; + public float Padding { get; set; } = 4.0f; + + public QuikFont Font { get; set; } + + public QuikStrokeStyle NormalStroke { get; set; } + public QuikFillStyle NormalFill { get; set; } + public QuikStrokeStyle HoverStroke { get; set; } + public QuikFillStyle HoverFill { get; set; } + public QuikStrokeStyle ActiveStroke { get; set; } + public QuikFillStyle ActiveFill { get; set; } + + private ButtonClass _class = ButtonClass.Normal; + + private enum ButtonClass + { + Normal, + Hover, + Active + } + + + protected override void OnMouseEnter(MouseMoveEventArgs args) + { + base.OnMouseEnter(args); + + if (_class == ButtonClass.Normal) + _class = ButtonClass.Hover; + } + + protected override void OnMouseLeave(MouseMoveEventArgs args) + { + base.OnMouseLeave(args); + + if (_class == ButtonClass.Hover) + _class = ButtonClass.Normal; + } + + protected override void OnMouseDown(MouseButtonEventArgs args) + { + base.OnMouseDown(args); + + if (_class == ButtonClass.Hover) + _class = ButtonClass.Active; + } + + protected override void OnMouseUp(MouseButtonEventArgs args) + { + base.OnMouseUp(args); + + if (_class == ButtonClass.Active) + { + _class = ButtonClass.Hover; + } + } + + protected override void OnPaint(QuikDraw draw) + { + QuikRectangle bounds = AbsoluteBounds; + + switch (_class) + { + default: + case ButtonClass.Normal: + draw.Commands.Enqueue(new QuikCommandRectangle(bounds) { + StrokeStyle = NormalStroke, + FillStyle = NormalFill + }); + break; + case ButtonClass.Hover: + draw.Commands.Enqueue(new QuikCommandRectangle(bounds) { + StrokeStyle = HoverStroke, + FillStyle = HoverFill + }); + break; + case ButtonClass.Active: + draw.Commands.Enqueue(new QuikCommandRectangle(bounds) { + StrokeStyle = ActiveStroke, + FillStyle = ActiveFill + }); + break; + } + + // Position the text so that it is centered. + float ascender = Root.Context.DefaultFont.Ascender; + float descender = -Root.Context.DefaultFont.Descender; + QuikVec2 position = + bounds.Min + + new QuikVec2( + Padding, + ( + (bounds.Size.Y - ascender - descender) / + 2) + + descender); + + draw.PutText(Text, position); + } + } +} \ No newline at end of file diff --git a/Quik/Controls/Container.cs b/Quik/Controls/Container.cs new file mode 100644 index 0000000..e86ae3b --- /dev/null +++ b/Quik/Controls/Container.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Quik.Controls +{ + public class Container : Control, IList + { + private List _children = new List(); + public IEnumerator GetEnumerator() => _children.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public virtual void Add(Control item) + { + _children.Add(item); + item.NotifyParentChanged(new ParentChangedEventArgs(this)); + } + + public virtual void Clear() + { + foreach (Control child in _children) + { + child.NotifyParentChanged(ParentChangedEventArgs.Disowned); + } + _children.Clear(); + } + + public bool Contains(Control item) => _children.Contains(item); + + public void CopyTo(Control[] array, int arrayIndex) => _children.CopyTo(array, arrayIndex); + + public virtual bool Remove(Control item) + { + if (_children.Remove(item)) + { + item.NotifyParentChanged(ParentChangedEventArgs.Disowned); + + return true; + } + else + { + return false; + } + } + + public int Count => _children.Count; + public bool IsReadOnly => false; + + public int IndexOf(Control item) =>_children.IndexOf(item); + + public virtual void Insert(int index, Control item) + { + _children.Insert(index, item); + NotifyParentChanged(new ParentChangedEventArgs(this)); + } + + public virtual void RemoveAt(int index) + { + _children.RemoveAt(index); + _children[index].NotifyParentChanged(ParentChangedEventArgs.Disowned); + } + + public virtual Control this[int index] + { + get => _children[index]; + set + { + _children[index].NotifyParentChanged(ParentChangedEventArgs.Disowned); + _children[index] = value; + value.NotifyParentChanged(new ParentChangedEventArgs(this)); + } + } + + public Control this[string name] => _children.Find(x => x.Name == name); + + internal override void NotifyUpdate() + { + base.NotifyUpdate(); + + foreach (Control child in _children) + { + child.NotifyUpdate(); + } + } + + internal override void NotifyPaint(QuikDraw draw) + { + base.NotifyPaint(draw); + + foreach (Control child in _children) + { + child.NotifyPaint(draw); + } + } + + internal override void NotifyRootChanged(RootChangedEventArgs args) + { + base.NotifyRootChanged(args); + + foreach (Control child in _children) + { + child.NotifyRootChanged(args); + } + } + + internal override void NotifyMouseMove(MouseMoveEventArgs args) + { + base.NotifyMouseMove(args); + + foreach (Control child in _children) + { + child.NotifyMouseMove(args); + } + } + + internal override void NotifyMouseDown(MouseButtonEventArgs args) + { + base.NotifyMouseDown(args); + + foreach (Control child in _children) + { + child.NotifyMouseDown(args); + } + } + + internal override void NotifyMouseUp(MouseButtonEventArgs args) + { + base.NotifyMouseUp(args); + + foreach (Control child in _children) + { + child.NotifyMouseUp(args); + } + } + } +} \ No newline at end of file diff --git a/Quik/Controls/Control.cs b/Quik/Controls/Control.cs new file mode 100644 index 0000000..7f3aef0 --- /dev/null +++ b/Quik/Controls/Control.cs @@ -0,0 +1,153 @@ +using System; + +namespace Quik.Controls +{ + public class Control + { + public string Name { get; set; } = null; + + public Control Parent { get; set; } = null; + + protected RootControl Root { get; set; } = null; + + public QuikRectangle Bounds { get; set; } + + public bool Focused { get => Root.FocusedControl == this; } + + public QuikRectangle AbsoluteBounds + { + get => Parent is null + ? Bounds + : new QuikRectangle(Parent.Bounds.Min + Bounds.Max, Parent.Bounds.Min + Bounds.Min); + set => Bounds = Parent is null + ? value + : new QuikRectangle(value.Min - Parent.Bounds.Min, value.Max - Parent.Bounds.Min); + } + + private MouseButton DownButtons; + + // Hierarchy events. + + public event EventHandler Update; + public event EventHandler Paint; + public event EventHandler RootChanging; + public event EventHandler ParentChanging; + public event EventHandler FocusLost; + public event EventHandler FocusAcquired; + + // Mouse events. + + public event EventHandler Clicked; + public event EventHandler MouseDown; + public event EventHandler MouseUp; + public event EventHandler MouseEnter; + public event EventHandler MouseMove; + // public event EventHandler MouseHover; + public event EventHandler MouseLeave; + + protected virtual void OnUpdate() => Update?.Invoke(this, EventArgs.Empty); + protected virtual void OnPaint(QuikDraw draw) => Paint?.Invoke(this, draw); + protected virtual void OnParentChanging(ParentChangedEventArgs args) => ParentChanging?.Invoke(this, args); + protected virtual void OnRootChanging(RootChangedEventArgs args) => RootChanging?.Invoke(this, args); + protected virtual void OnFocusLost(FocusChangedEventArgs args) => FocusLost?.Invoke(this, args); + protected virtual void OnFocusAcquired(FocusChangedEventArgs args) => FocusAcquired?.Invoke(this, args); + + protected virtual void OnClicked(MouseButtonEventArgs args) => Clicked?.Invoke(this, args); + protected virtual void OnMouseDown(MouseButtonEventArgs args) => MouseDown?.Invoke(this, args); + protected virtual void OnMouseUp(MouseButtonEventArgs args) => MouseUp?.Invoke(this, args); + protected virtual void OnMouseEnter(MouseMoveEventArgs args) => MouseEnter?.Invoke(this, args); + protected virtual void OnMouseMove(MouseMoveEventArgs args) => MouseMove?.Invoke(this, args); + // protected virtual void OnMouseHover(MouseMoveEventArgs args) => MouseHover?.Invoke(this, args); + protected virtual void OnMouseLeave(MouseMoveEventArgs args) => MouseLeave?.Invoke(this, args); + + public void Focus() + { + Root?.Focus(this); + } + + internal virtual void NotifyUpdate() + { + OnUpdate(); + } + + internal virtual void NotifyPaint(QuikDraw draw) + { + OnPaint(draw); + } + + internal void NotifyFocusChanged(FocusChangedEventArgs args) + { + if (args.Focused == this) + { + OnFocusAcquired(args); + } + else + { + OnFocusLost(args); + } + } + + internal virtual void NotifyParentChanged(ParentChangedEventArgs args) + { + OnParentChanging(args); + Parent = args.NewParent; + } + + internal virtual void NotifyRootChanged(RootChangedEventArgs args) + { + OnRootChanging(args); + Root = args.NewRoot; + } + + internal virtual void NotifyMouseMove(MouseMoveEventArgs args) + { + QuikRectangle bounds = AbsoluteBounds; + + if (bounds.Contains(args.AbsolutePosition)) + { + if (!bounds.Contains(args.AbsolutePosition - args.Motion)) + { + OnMouseEnter(args); + } + + OnMouseMove(args); + } + else if (bounds.Contains(args.AbsolutePosition - args.Motion)) + { + OnMouseLeave(args); + } + } + + internal virtual void NotifyMouseDown(MouseButtonEventArgs args) + { + if (AbsoluteBounds.Contains(args.AbsolutePosition)) + { + OnMouseDown(args); + DownButtons = args.Buttons; + } + } + + internal virtual void NotifyMouseUp(MouseButtonEventArgs args) + { + if (AbsoluteBounds.Contains(args.AbsolutePosition)) + { + MouseButton mask; + + OnMouseUp(args); + + if ((mask = DownButtons & args.Buttons) > 0) + { + MouseButtonEventArgs nargs = new MouseButtonEventArgs(args.AbsolutePosition, mask); + Focus(); + OnClicked(nargs); + } + + DownButtons &= ~mask; + } + else + { + DownButtons = 0; + } + } + } +} \ No newline at end of file diff --git a/Quik/Controls/HierarchyEvents.cs b/Quik/Controls/HierarchyEvents.cs new file mode 100644 index 0000000..afa35cc --- /dev/null +++ b/Quik/Controls/HierarchyEvents.cs @@ -0,0 +1,38 @@ +using System; + +namespace Quik.Controls +{ + public class ParentChangedEventArgs : EventArgs + { + public Control NewParent { get; } + + public ParentChangedEventArgs(Control newParent) + { + NewParent = newParent; + } + + public static ParentChangedEventArgs Disowned { get; } = new ParentChangedEventArgs(null); + } + + public class RootChangedEventArgs : EventArgs + { + public RootControl NewRoot { get; } + + public RootChangedEventArgs(RootControl newRoot) + { + NewRoot = newRoot; + } + + public static RootChangedEventArgs Disowned { get; } = new RootChangedEventArgs(null); + } + + public class FocusChangedEventArgs : EventArgs + { + public Control Focused { get; } + + public FocusChangedEventArgs(Control focused) + { + Focused = focused; + } + } +} \ No newline at end of file diff --git a/Quik/Controls/Label.cs b/Quik/Controls/Label.cs new file mode 100644 index 0000000..5e73f3b --- /dev/null +++ b/Quik/Controls/Label.cs @@ -0,0 +1,36 @@ +using Quik.Typography; + +namespace Quik.Controls +{ + public class Label : Control + { + public bool MultiLine { get; set; } = false; + public string Text { get; set; } = ""; + + public float Padding { get; set; } = 4.0f; + + public QuikFont Font { get; set; } + + protected override void OnPaint(QuikDraw draw) + { + if (MultiLine) + { + QuikRectangle absolute = AbsoluteBounds; + QuikVec2 paddingVector = new QuikVec2(Padding, Padding); + QuikRectangle rectangle; + rectangle.Min = absolute.Min + paddingVector; + rectangle.Max = absolute.Min - paddingVector; + + // FIXME: For now use a puttext command. + draw.PutText(Text, rectangle.Min + new QuikVec2(0, rectangle.Size.Y / 3f)); + + // draw.FlowText(Text, rectangle); + } + else + { + QuikVec2 position = AbsoluteBounds.Min + new QuikVec2(Padding, Padding + AbsoluteBounds.Size.Y / 3f); + draw.PutText(Text, position); + } + } + } +} \ No newline at end of file diff --git a/Quik/Controls/RootControl.cs b/Quik/Controls/RootControl.cs new file mode 100644 index 0000000..dae06d7 --- /dev/null +++ b/Quik/Controls/RootControl.cs @@ -0,0 +1,108 @@ + +namespace Quik.Controls +{ + public sealed class RootControl : Container + { + public QuikContext Context { get; } + public Control FocusedControl { get; private set; } + + public RootControl(QuikContext context) + { + Context = context; + } + + public void Focus(Control which) + { + FocusChangedEventArgs args = new FocusChangedEventArgs(which); + + FocusedControl?.NotifyFocusChanged(args); + (FocusedControl = which).NotifyFocusChanged(args); + } + + public new void NotifyUpdate() + { + base.NotifyUpdate(); + } + + public new void NotifyPaint(QuikDraw draw) + { + base.NotifyPaint(draw); + } + + private MouseState LastMouseState; + public void NotifyMouse(in MouseState state) + { + MouseMoveEventArgs move = new MouseMoveEventArgs( + state.AbsolutePosition, + LastMouseState.AbsolutePosition); + MouseButtonEventArgs up = new MouseButtonEventArgs( + state.AbsolutePosition, + LastMouseState.ButtonsDown & ~state.ButtonsDown); + MouseButtonEventArgs down = new MouseButtonEventArgs( + state.AbsolutePosition, + ~LastMouseState.ButtonsDown & state.ButtonsDown); + + if (move.Motion.Magnitude > 0) + NotifyMouseMove(move); + + if (up.Buttons != 0) + NotifyMouseUp(up); + + if (down.Buttons != 0) + NotifyMouseDown(down); + + LastMouseState = state; + } + + public override void Add(Control item) + { + base.Add(item); + item.NotifyRootChanged(new RootChangedEventArgs(this)); + } + + public override void Clear() + { + foreach (Control child in this) + { + child.NotifyRootChanged(RootChangedEventArgs.Disowned); + } + base.Clear(); + } + + public override void Insert(int index, Control item) + { + base.Insert(index, item); + item.NotifyRootChanged(new RootChangedEventArgs(this)); + } + + public override bool Remove(Control item) + { + if(base.Remove(item)) + { + item.NotifyRootChanged(RootChangedEventArgs.Disowned); + return true; + } + else + { + return false; + } + } + + public override void RemoveAt(int index) + { + this[index].NotifyRootChanged(RootChangedEventArgs.Disowned); + base.RemoveAt(index); + } + + public override Control this[int index] + { + get => base[index]; + set + { + base[index].NotifyRootChanged(RootChangedEventArgs.Disowned); + base[index] = value; + value.NotifyRootChanged(new RootChangedEventArgs(this)); + } + } + } +} \ No newline at end of file diff --git a/Quik/Mouse.cs b/Quik/Mouse.cs new file mode 100644 index 0000000..146a063 --- /dev/null +++ b/Quik/Mouse.cs @@ -0,0 +1,74 @@ +using System; + +namespace Quik +{ + public enum MouseButton + { + Primary = 1 << 0, + Secondary = 1 << 1, + Tertiary = 1 << 2, + Auxilliary1 = 1 << 3, + Auxilliary2 = 1 << 4, + Auxilliary3 = 1 << 5, + Auxilliary4 = 1 << 6, + Auxilliary5 = 1 << 8, + } + + public struct MouseState + { + public readonly QuikVec2 AbsolutePosition; + public readonly MouseButton ButtonsDown; + + public MouseState(QuikVec2 position, MouseButton down) + { + AbsolutePosition = position; + ButtonsDown = down; + } + } + + public class MouseButtonEventArgs : EventArgs + { + public QuikVec2 AbsolutePosition { get; } + public MouseButton Buttons { get; } + + public MouseButtonEventArgs(QuikVec2 position, MouseButton buttons) + { + AbsolutePosition = position; + Buttons = buttons; + } + + public QuikVec2 RelativePosition(QuikVec2 origin) + { + return AbsolutePosition - origin; + } + + public QuikVec2 RelativePosition(Controls.Control control) + { + return AbsolutePosition - control.AbsoluteBounds.Min; + } + } + + public class MouseMoveEventArgs : EventArgs + { + public QuikVec2 AbsolutePosition { get; } + public QuikVec2 LastPosition { get; } + public QuikVec2 Motion { get; } + + public MouseMoveEventArgs(QuikVec2 position, QuikVec2 lastPosition) + { + AbsolutePosition = position; + LastPosition = lastPosition; + Motion = position - lastPosition; + } + + public QuikVec2 RelativePosition(QuikVec2 origin) + { + return AbsolutePosition - origin; + } + + public QuikVec2 RelativePosition(Controls.Control control) + { + return AbsolutePosition - control.AbsoluteBounds.Min; + } + } +} \ No newline at end of file diff --git a/Quik/QuikCommand.cs b/Quik/QuikCommand.cs index c235de5..2187dd4 100644 --- a/Quik/QuikCommand.cs +++ b/Quik/QuikCommand.cs @@ -82,6 +82,8 @@ namespace Quik /// FlowText, + EmitTypeset, + /// /// Clear the image mask. /// @@ -568,6 +570,43 @@ namespace Quik } } + /// + /// Emit previously typeset text. + /// + public sealed class QuikCommandEmitText : QuikCommand + { + /// + public override QuikCommandType Type => QuikCommandType.EmitTypeset; + + /// + /// The typeset group to emit. + /// + public Typography.TypesetGroup Group { get; } + + public QuikVec2 Offset { get; } + + /// + /// Create an emit typeset text command. + /// + /// The typeset group to emit. + public QuikCommandEmitText(Typography.TypesetGroup group) + { + Group = group; + Offset = new QuikVec2(0, 0); + } + + /// + /// Create an emit typeset text command. + /// + /// The typeset group to emit. + /// The offset to emit at. + public QuikCommandEmitText(Typography.TypesetGroup group, QuikVec2 offset) + { + Group = group; + Offset = offset; + } + } + /// /// Clear the stencil buffer. /// diff --git a/Quik/Typography/QuikFont.cs b/Quik/Typography/QuikFont.cs index 18baf64..44636f6 100644 --- a/Quik/Typography/QuikFont.cs +++ b/Quik/Typography/QuikFont.cs @@ -8,6 +8,9 @@ namespace Quik.Typography public float FontSize => Style.Size; public QuikFontStyle FontStyle => Style; + public abstract float Ascender { get; } + public abstract float Descender { get; } + public abstract bool HasCharacter(int character); public abstract void GetCharacter(int character, out IQuikTexture texture, out QuikGlyph glyph); } diff --git a/Quik/Typography/QuikFontStyle.cs b/Quik/Typography/QuikFontStyle.cs index 78c4468..6ee7ae9 100644 --- a/Quik/Typography/QuikFontStyle.cs +++ b/Quik/Typography/QuikFontStyle.cs @@ -12,6 +12,33 @@ namespace Quik.Typography Size = size; Type = type; } + + public override int GetHashCode() + { + return + Family.GetHashCode() ^ + (Type.GetHashCode() * 1303) ^ + (Size.GetHashCode() * 2447); + } + + public override bool Equals(object obj) + { + return + obj is QuikFontStyle other && + other.Family == Family && + other.Size == Size && + other.Type == Type; + } + + public static bool operator==(QuikFontStyle a, QuikFontStyle b) + { + return a.Size == b.Size && a.Type == b.Type && a.Family == b.Family; + } + + public static bool operator !=(QuikFontStyle a, QuikFontStyle b) + { + return a.Size != b.Size || a.Type != b.Type || a.Family != b.Family; + } } public enum QuikFontType diff --git a/Quik/Typography/TextLayout.cs b/Quik/Typography/TextLayout.cs new file mode 100644 index 0000000..1c7490c --- /dev/null +++ b/Quik/Typography/TextLayout.cs @@ -0,0 +1,508 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +namespace Quik.Typography +{ + /// + /// An atomic horizontal block of text which cannot be further divided. + /// + public struct HorizontalTextBlock + { + /// + /// The font associated with the text block. + /// + /// + public QuikFont Font { get; } + /// + /// Textual contents of the text block. + /// + public string Text { get; } + /// + /// Indicates this text block should be layed out right to left. + /// + public bool IsRTL { get; } + + /// + /// Indicates this is a whitespace block. + /// + public bool IsWhitespace => string.IsNullOrWhiteSpace(Text); + + public float Width { get; } + public float Ascend { get; } + public float Descend { get; } + public float Height => Ascend - Descend; + + public HorizontalTextBlock(QuikFont font, string text, bool rtl = false) + { + Font = font; + Text = text; + IsRTL = rtl; + + float width = 0.0f; + float ascend = 0.0f; + float descend = 0.0f; + + foreach (char chr in text) + { + font.GetCharacter(chr, out _, out QuikGlyph glyph); + width += glyph.Advance.X; + ascend = Math.Max(ascend, glyph.HorizontalBearing.Y); + descend = Math.Min(descend, glyph.HorizontalBearing.Y - glyph.Size.Y); + } + + Width = width; + Ascend = ascend; + Descend = descend; + } + + public HorizontalTextBlock(float width) + { + Font = null; + Text = string.Empty; + IsRTL = false; + Width = width; + Ascend = Descend = 0.0f; + } + } + + /// + /// An atomic vertical block of text which cannot be further divided. + /// + public struct VerticalTextBlock + { + public QuikFont Font { get; } + public string Text { get; } + public bool IsWhitespace => string.IsNullOrWhiteSpace(Text); + public float Width { get; } + public float Height { get; } + + public VerticalTextBlock(QuikFont font, string text) + { + Font = font; + Text = text; + + float width = 0.0f; + float height = 0.0f; + + foreach(char chr in text) + { + font.GetCharacter(chr, out _, out QuikGlyph glyph); + width = Math.Max(width, - glyph.VerticalBearing.X * 2); + height += glyph.Advance.Y; + } + + Width = width; + Height = height; + } + + public VerticalTextBlock(float height) + { + Font = null; + Text = string.Empty; + Width = 0.0f; + Height = height; + } + } + + public abstract class Paragraph + { + public abstract bool IsVertical { get; } + + public float JustifyLimit { get; set; } = 30.0f; + public TextAlignment Alignment { get; set; } = TextAlignment.Default; + public float PreSpace { get; set; } = 0.0f; + public float PostSpace { get; set; } = 0.0f; + public float FirstLineInset { get; set; } = 0.0f; + public float LineGap { get; set; } = 12.0f; + + public abstract void Typeset(TypesetGroup group, float width); + + protected abstract void AppendBlock(QuikFont font, string text, bool rtl = false); + + public void ConsumeText(QuikFont font, string text) + { + StringBuilder segment = new StringBuilder(); + bool rtl = false; + bool ws = false; + + foreach(char chr in text) + { + UnicodeCategory cat = char.GetUnicodeCategory(chr); + // FIXME: don't ignore control characters like a barbarian. + // TODO: how do I detect text flow direction??? + if (char.IsWhiteSpace(chr) && chr != '\u00a0') + { + if (ws) + { + segment.Append(chr); + } + else + { + AppendBlock(font, segment.ToString()); + segment.Clear(); + segment.Append(chr); + ws = true; + } + } + else + { + if (ws) + { + AppendBlock(font, segment.ToString(), rtl); + segment.Clear(); + segment.Append(chr); + ws = false; + } + else + { + segment.Append(chr); + } + } + } + + if (segment.Length > 0) + { + AppendBlock(font, segment.ToString(), rtl); + } + } + } + + public class HorizontalParagraph : Paragraph + { + public override bool IsVertical => false; + public List Blocks { get; } = new List(); + + public override void Typeset(TypesetGroup group, float width) + { + Queue line = new Queue(); + int index = 0; + bool firstLine = true; + + QuikVec2 pen = new QuikVec2(0, -PreSpace); + + while (index < Blocks.Count) + { + index + += GatherLine( + index, + width - (firstLine ? FirstLineInset : 0), + line, + out float excess, + out float ascend, + out float descend); + + firstLine = false; + + pen.Y -= ascend; + + float interblockWs = + Alignment.HasFlag(TextAlignment.Justify) && excess < JustifyLimit + ? excess / (line.Count - 1) + : 0.0f; + + switch (Alignment & TextAlignment.HorizontalMask) + { + default: + case TextAlignment.AlignLeft: + if (firstLine) pen.X += FirstLineInset; + break; + case TextAlignment.AlignCenterH: + pen.X += excess / 2; + break; + case TextAlignment.AlignRight: + pen.X += excess; + break; + } + + PutBlock(group, line, interblockWs, ref pen); + + pen.Y -= LineGap - descend; + pen.X = 0.0f; + } + + pen.Y -= PostSpace; + + group.BoundingBox = new QuikRectangle(width, 0, 0, pen.Y); + group.Translate(-pen); + } + + private int GatherLine( + int index, + float width, + Queue line, + out float excess, + out float ascend, + out float descend) + { + float currentWidth = 0.0f; + ascend = descend = 0.0f; + + for (int i = index; i < Blocks.Count; i++) + { + HorizontalTextBlock block = Blocks[i]; + + if (currentWidth + block.Width > width) + { + break; + } + + ascend = Math.Max(ascend, block.Ascend); + descend = Math.Min(descend, block.Descend); + currentWidth += block.Width; + line.Enqueue(block); + } + + excess = width - currentWidth; + return line.Count; + } + + public void PutBlock( + TypesetGroup group, + Queue line, + float interblockWs, + ref QuikVec2 pen) + { + QuikVec2 penpal = pen; + + while (line.TryDequeue(out HorizontalTextBlock block)) + { + if (block.IsWhitespace) + { + penpal.X += block.Width + interblockWs; + continue; + } + + if (block.IsRTL) + { + for (int i = block.Text.Length - 1; i >= 0; i--) + { + char chr = block.Text[i]; + block.Font.GetCharacter(chr, out IQuikTexture texture, out QuikGlyph metrics); + group.Add( + new TypesetCharacter( + chr, + texture, + new QuikRectangle( + penpal.X + metrics.Advance.X, + penpal.Y + metrics.HorizontalBearing.Y, + penpal.X + metrics.HorizontalBearing.X, + penpal.Y - metrics.Size.Y + metrics.HorizontalBearing.Y), + metrics.Location + ) + ); + penpal.X += metrics.Advance.X; + } + } + else + { + for (int i = 0; i < block.Text.Length; i++) + { + char chr = block.Text[i]; + block.Font.GetCharacter(chr, out IQuikTexture texture, out QuikGlyph metrics); + group.Add( + new TypesetCharacter( + chr, + texture, + new QuikRectangle( + penpal.X + metrics.Advance.X, + penpal.Y + metrics.HorizontalBearing.Y, + penpal.X + metrics.HorizontalBearing.X, + penpal.Y - metrics.Size.Y + metrics.HorizontalBearing.Y), + metrics.Location + ) + ); + + penpal.X += metrics.Advance.X; + } + } + + penpal.X += interblockWs; + } + + penpal.X -= interblockWs; + + pen = penpal; + } + + protected override void AppendBlock(QuikFont font, string text, bool rtl = false) + { + Blocks.Add(new HorizontalTextBlock(font, text, rtl)); + } + } + + public class VerticalParagraph : Paragraph + { + public override bool IsVertical => true; + public List Blocks { get; } = new List(); + + public override void Typeset(TypesetGroup group, float width) + { + throw new NotImplementedException(); + } + + protected override void AppendBlock(QuikFont font, string text, bool rtl = false) + { + Blocks.Add(new VerticalTextBlock(font, text)); + } + } + + public struct TypesetCharacter + { + public int Character; + public IQuikTexture Texture; + public QuikRectangle Position; + public QuikRectangle UV; + + public TypesetCharacter( + int chr, + IQuikTexture texture, + in QuikRectangle position, + in QuikRectangle uv) + { + Character = chr; + Texture = texture; + Position = position; + UV = uv; + } + } + + public class TypesetGroup : ICollection + { + private int _count = 0; + private TypesetCharacter[] _array = Array.Empty(); + + public QuikRectangle BoundingBox; + + public int Count => _count; + + public bool IsReadOnly => false; + + public void Add(TypesetCharacter item) + { + if (_count == _array.Length) + { + Array.Resize(ref _array, _array.Length + 256); + } + + _array[_count++] = item; + } + + public void Clear() + { + _count = 0; + } + + public void Translate(QuikVec2 offset) + { + BoundingBox.Translate(offset); + + for (int i = 0; i < _count; i++) + { + _array[i].Position.Translate(offset); + } + } + + public bool Contains(TypesetCharacter item) + { + throw new NotSupportedException(); + } + + public void CopyTo(TypesetCharacter[] array, int arrayIndex) + { + _array.CopyTo(array, arrayIndex); + } + + public IEnumerator GetEnumerator() + { + for (int i = 0; i < _count; i++) + { + yield return _array[i]; + } + } + + public bool Remove(TypesetCharacter item) + { + throw new NotSupportedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return (IEnumerator)GetEnumerator(); + } + + public void SortBy(IComparer comparer) + { + Array.Sort(_array, 0, _count, comparer); + } + + public static IComparer SortByTexture { get; } = new SortByTextureComparer(); + + private class SortByTextureComparer : IComparer + { + public int Compare(TypesetCharacter x, TypesetCharacter y) + { + return y.Texture.GetHashCode() - x.Texture.GetHashCode(); + } + } + } + + /// + /// An enumeration of possible text alignments. + /// + [Flags] + public enum TextAlignment + { + /// + /// Align to the left margin, horizontally. + /// + AlignLeft = 0x01, + + /// + /// Align text to center of left and right margins. + /// + AlignCenterH = 0x03, + + /// + /// Align to the right margin, horizontally. + /// + AlignRight = 0x02, + + /// + /// A bitmask for values relating to horizontal alignment. + /// + HorizontalMask = 0x03, + + /// + /// Align text to the top margin. + /// + AlignTop = 0x00, + + /// + /// Align text between the top and bottom margins. + /// + AlignCenterV = 0x04, + + /// + /// Align text to the bottom margin. + /// + AlignBottom = 0x08, + + /// + /// A bitmask for values relating to the vertical alignment. + /// + VerticalMask = 0x0C, + + /// + /// Distribute characters uniformly on the line, when possible. + /// + Justify = 0x10, + + /// + /// The default text alignment value. + /// + Default = AlignTop | AlignLeft + } +} \ No newline at end of file diff --git a/Quik/Typography/UnicodeUtil.cs b/Quik/Typography/UnicodeUtil.cs new file mode 100644 index 0000000..301f08c --- /dev/null +++ b/Quik/Typography/UnicodeUtil.cs @@ -0,0 +1,108 @@ +using System; +using System.Text; + +namespace Quik.Typography +{ + public static class UnicodeUtil + { + + public static bool IsWhiteSpace(int chr) + { + switch (chr) + { + case '\t': case '\n': case '\v': case '\f': case '\r': case ' ': + case '\u0085': case '\u00A0': case '\u1680': case '\u2000': + case '\u2001': case '\u2002': case '\u2003': case '\u2004': + case '\u2005': case '\u2006': case '\u2007': case '\u2008': + case '\u2009': case '\u200A': case '\u2028': case '\u2029': + case '\u202F': case '\u205F': case '\u3000': case '\u180E': + case '\u200B': case '\u200C': case '\u200D': case '\u2060': + return true; + default: + return false; + } + } + + public static bool IsUnicodeWhitespace(int chr) + { + switch (chr) + { + case '\t': case '\n': case '\v': case '\f': case '\r': case ' ': + case '\u0085': case '\u00A0': case '\u1680': case '\u2000': + case '\u2001': case '\u2002': case '\u2003': case '\u2004': + case '\u2005': case '\u2006': case '\u2007': case '\u2008': + case '\u2009': case '\u200A': case '\u2028': case '\u2029': + case '\u202F': case '\u205F': case '\u3000': + return true; + default: + return false; + } + } + + public static bool IsNewLine(int chr) + { + switch (chr) + { + case '\n': case '\v': case '\f': case '\r': + case '\u0085': case '\u2028': case '\u2029': + return true; + default: + return false; + } + } + + public static WhitespaceInfo GetWhitespaceInfo(int chr) + { + switch (chr) + { + // CHAR ------------BOILERPLATE----------- BREAK NEWLINE + case '\t': return new WhitespaceInfo('\t', true, false); + case '\n': return new WhitespaceInfo('\n', false, true); + case '\v': return new WhitespaceInfo('\v', false, true); + case '\f': return new WhitespaceInfo('\f', false, true); + case '\r': return new WhitespaceInfo('\r', false, true); + case ' ': return new WhitespaceInfo(' ', true, false); + case '\u0085': return new WhitespaceInfo('\u0085', false, true); + case '\u00A0': return new WhitespaceInfo('\u00A0', false, false); + case '\u1680': return new WhitespaceInfo('\u1689', true, false); + case '\u2000': return new WhitespaceInfo('\u2000', true, false); + case '\u2001': return new WhitespaceInfo('\u2001', true, false); + case '\u2002': return new WhitespaceInfo('\u2002', true, false); + case '\u2003': return new WhitespaceInfo('\u2003', true, false); + case '\u2004': return new WhitespaceInfo('\u2004', true, false); + case '\u2005': return new WhitespaceInfo('\u2005', true, false); + case '\u2006': return new WhitespaceInfo('\u2006', true, false); + case '\u2007': return new WhitespaceInfo('\u2007', false, false); + case '\u2008': return new WhitespaceInfo('\u2008', true, false); + case '\u2009': return new WhitespaceInfo('\u2009', true, false); + case '\u200A': return new WhitespaceInfo('\u200A', true, false); + case '\u2028': return new WhitespaceInfo('\u2028', false, true); + case '\u2029': return new WhitespaceInfo('\u2029', false, true); + case '\u202F': return new WhitespaceInfo('\u202F', false, false); + case '\u205F': return new WhitespaceInfo('\u205F', true, false); + case '\u3000': return new WhitespaceInfo('\u3000', true, false); + case '\u180E': return new WhitespaceInfo('\u180E', true, false); + case '\u200B': return new WhitespaceInfo('\u200B', true, false); + case '\u200C': return new WhitespaceInfo('\u200C', true, false); + case '\u200D': return new WhitespaceInfo('\u200D', true, false); + case '\u2060': return new WhitespaceInfo('\u2060', false, false); + default: + throw new ArgumentException("Character is not a whitespace character.", nameof(chr)); + } + } + } + + public struct WhitespaceInfo + { + public int Character { get; } + public bool IsBreaking { get; } + public bool IsNewline { get; } + + public WhitespaceInfo(int chr, bool breaking, bool nl) + { + Character = chr; + IsBreaking = breaking; + IsNewline = nl; + } + } +} \ No newline at end of file diff --git a/Quik/VertexGenerator/QuikVertexGenerator.cs b/Quik/VertexGenerator/QuikVertexGenerator.cs index 70cbf86..4405125 100644 --- a/Quik/VertexGenerator/QuikVertexGenerator.cs +++ b/Quik/VertexGenerator/QuikVertexGenerator.cs @@ -79,7 +79,7 @@ namespace Quik.VertexGenerator public List DrawCalls { get; } = new List(); - public float CurveGranularity { get; set; } = 0.2f; + public float CurveGranularity { get; set; } = 1f; public QuikContext Context { get; } @@ -233,6 +233,10 @@ namespace Quik.VertexGenerator RenderTextPut(command as QuikCommandPutText); goto exit_with_call; + case QuikCommandType.EmitTypeset: + RenderTextTypeset(command as QuikCommandEmitText); + goto exit_with_call; + default: { if (OnHandleCommand(this, command)) @@ -1468,7 +1472,7 @@ namespace Quik.VertexGenerator a.Position = pointer + new QuikVec2(0, metrics.HorizontalBearing.Y - metrics.Size.Y); a.TextureCoordinates = metrics.Location.Min; - + b.Position = a.Position + new QuikVec2(metrics.Size.X, 0); c.Position = a.Position + metrics.Size; d.Position = a.Position + new QuikVec2(0, metrics.Size.Y); @@ -1478,7 +1482,7 @@ namespace Quik.VertexGenerator d.TextureCoordinates = new QuikVec2(metrics.Location.Left, metrics.Location.Top); pointer.X += metrics.Advance.X; - + short startVertex = (short)_vertexBufferPointer; AddVertex(a, b, c, d); AddElement(startVertex, (short)(startVertex + 1), (short)(startVertex + 2), startVertex, (short)(startVertex + 2), (short)(startVertex + 3)); @@ -1492,6 +1496,61 @@ namespace Quik.VertexGenerator DrawCalls.Add(call); } } + + private void RenderTextTypeset(QuikCommandEmitText text) + { + short startElement = (short)_elementBufferPointer; + TypesetGroup group = text.Group; + QuikVertex vertex = new QuikVertex() { Color = new QuikColor(0x000000ff) }; + IQuikTexture texture = null; + + group.SortBy(TypesetGroup.SortByTexture); + foreach (TypesetCharacter chr in group) + { + if (texture == null) + { + texture = chr.Texture; + } + else if (texture != chr.Texture) + { + EmitCall(); + + startElement = (short)_elementBufferPointer; + texture = chr.Texture; + + CallTemplate.ClearStencil = false; + } + + QuikVertex a, b, c, d; + a = b = c = d = vertex; + + a.Position = new QuikVec2(chr.Position.Left, chr.Position.Bottom) + text.Offset; + b.Position = new QuikVec2(chr.Position.Right, chr.Position.Bottom) + text.Offset; + c.Position = new QuikVec2(chr.Position.Right, chr.Position.Top) + text.Offset; + d.Position = new QuikVec2(chr.Position.Left, chr.Position.Top) + text.Offset; + + a.TextureCoordinates = new QuikVec2(chr.UV.Left, chr.UV.Bottom); + b.TextureCoordinates = new QuikVec2(chr.UV.Right, chr.UV.Bottom); + c.TextureCoordinates = new QuikVec2(chr.UV.Right, chr.UV.Top); + d.TextureCoordinates = new QuikVec2(chr.UV.Left, chr.UV.Top); + + short startVertex = (short)_vertexBufferPointer; + AddVertex(a, b, c, d); + AddElement(startVertex, (short)(startVertex + 1), (short)(startVertex + 2), startVertex, (short)(startVertex + 2), (short)(startVertex + 3)); + } + + EmitCall(); + + void EmitCall() + { + QuikDrawCall call = CallTemplate; + + call.Texture = texture; + call.Offset = (short)(startElement * 2); + call.Count = (short)(_elementBufferPointer - startElement); + DrawCalls.Add(call); + } + } #endregion } diff --git a/QuikTestApplication/Program.cs b/QuikTestApplication/Program.cs index d47a8d2..0aed45f 100644 --- a/QuikTestApplication/Program.cs +++ b/QuikTestApplication/Program.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using Quik; using Quik.VertexGenerator; using OpenTK.Graphics.OpenGL4; @@ -7,8 +8,10 @@ using OpenTK.Mathematics; using OpenTK.Windowing.Common; using OpenTK.Windowing.Desktop; using OpenTK.Windowing.GraphicsLibraryFramework; +using Quik.FreeType; using Quik.OpenTK; using Quik.Typography; +using Quik.Controls; namespace QuikTestApplication { @@ -38,25 +41,58 @@ in vec4 fcolor; out vec4 outcolor; uniform sampler2D texture0; +uniform vec2 texture0offset; void main() { - outcolor = fcolor * texture(texture0, ftexcoord); + outcolor = fcolor * texture(texture0, ftexcoord + texture0offset); } "; - + public static void Main(string[] args) { NativeWindowSettings windowSettings = NativeWindowSettings.Default; windowSettings.NumberOfSamples = 4; NativeWindow window = new NativeWindow(windowSettings); - + window.Context.MakeCurrent(); GL.LoadBindings(new GLFWBindingsContext()); - QuikContext context = new QuikContext(new OpenGLTextureManager(), new TextFontManager()); + FreeTypeFontManager fontManager = new FreeTypeFontManager(); + QuikContext context = new QuikContext(new OpenGLTextureManager(), fontManager); QuikVertexGenerator gen = new QuikVertexGenerator(context); - + RootControl root = new RootControl(context); + Button button = new Button() + { + Bounds = new QuikRectangle(120, 60, 20, 20), + Text = "button", + Padding = 8, + NormalStroke = new QuikStrokeStyle(new QuikColor(0xccccccff), 4f), + HoverStroke = new QuikStrokeStyle(new QuikColor(0x1010ccff), 4f), + ActiveStroke = new QuikStrokeStyle(new QuikColor(0x999999ff), 4f), + }; + button.Clicked += (sender, args) => { + if (!args.Buttons.HasFlag(Quik.MouseButton.Primary)) + return; + + Button xbutton = (Button)sender; + + if (xbutton.Text == "button") + { + xbutton.Text = "le button"; + } + else + { + xbutton.Text = "button"; + } + }; + root.MouseEnter += (_,_) => Console.WriteLine("enter"); + root.MouseLeave += (_,_) => Console.WriteLine("leave"); + root.MouseMove += (_,_) => Console.WriteLine("move"); + root.MouseDown += (_,_) => Console.WriteLine("down"); + root.MouseUp += (_,_) => Console.WriteLine("up"); + root.Add(button); + GL.Enable(EnableCap.Multisample); int sp; @@ -79,6 +115,8 @@ void main() GL.UseProgram(sp); } + + new GL30Driver(); int vbo, ebo, vao; vbo = GL.GenBuffer(); @@ -118,7 +156,7 @@ void main() loc = GL.GetUniformLocation(sp, "matrix"); - int i = 0; + int offsetLoc = GL.GetUniformLocation(sp, "texture0offset"); QuikStrokeStyle strokeBorder = new QuikStrokeStyle() { @@ -137,6 +175,10 @@ void main() Color = new QuikColor(0xeeeeeeff) }; + QuikFillStyle magenta = new QuikFillStyle + { + Color = new QuikColor(0xff00ffff) + }; int whiteTexture = GL.GenTexture(); uint[] whitePixel = {0xFFFFFFFF}; @@ -145,13 +187,46 @@ void main() GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)All.Linear); GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)All.Nearest); - context.DefaultFont = context.FontManager.GetFont(null); + context.DefaultFont = context.FontManager.GetFont(new QuikFontStyle("Arial", 16, QuikFontType.Normal)); + + const string testString = + "The quick brown fox jumps over the lazy dog. " + + "Sphinx of black quartz judge my vow. " + + "Have some japanese for fun: これが読めるかな?"; + + var para = new HorizontalParagraph(); + para.ConsumeText(context.DefaultFont, testString); + var typeset = new TypesetGroup(); + para.Typeset(typeset, 200); window.Context.SwapInterval = 0; - + Stopwatch stopwatch = Stopwatch.StartNew(); + float lastMs = stopwatch.ElapsedMilliseconds; + int frames = 0; for (;!window.IsExiting;) { NativeWindow.ProcessWindowEvents(false); + window.ProcessEvents(0.0f); + window.TryGetCurrentMonitorDpi(out float dpi, out _); + + if (window.IsFocused) + { + var mouseState = window.MouseState; + QuikVec2 postion = new QuikVec2(mouseState.Position.X, window.ClientSize.Y - mouseState.Position.Y) * (dpi/72f); + Quik.MouseButton buttons = + (mouseState.IsButtonDown(OpenTK.Windowing.GraphicsLibraryFramework.MouseButton.Button1) ? Quik.MouseButton.Primary : 0) | + (mouseState.IsButtonDown(OpenTK.Windowing.GraphicsLibraryFramework.MouseButton.Button2) ? Quik.MouseButton.Secondary : 0) | + (mouseState.IsButtonDown(OpenTK.Windowing.GraphicsLibraryFramework.MouseButton.Button3) ? Quik.MouseButton.Tertiary : 0); + + root.NotifyMouse(new Quik.MouseState(postion, buttons)); + } + + root.Bounds = new QuikRectangle( + window.ClientSize.X, + window.ClientSize.Y, + 0, + 0); + root.NotifyUpdate(); GL.Viewport(0, 0, window.Size.X, window.Size.Y); @@ -162,9 +237,9 @@ void main() Matrix4 matrix = Matrix4.CreateOrthographicOffCenter( 0, - (float)window.Size.X, + (float)window.Size.X*dpi/72f, 0, - (float)window.Size.Y, + (float)window.Size.Y*dpi/72f, 1, -1); GL.UniformMatrix4(loc, false, ref matrix); @@ -216,8 +291,11 @@ void main() FillStyle = fill, StrokeStyle = strokeBorder }); - - context.Draw.PutText("Aaah! the cat turned into a cat!", new QuikVec2(25,30)); + + // context.Draw.PutText("これが読めるかな?", new QuikVec2(25,30)); + root.NotifyPaint(context.Draw); + + context.Draw.Commands.Enqueue(new QuikCommandEmitText(typeset, new QuikVec2(200, 200))); QuikCommand command; while (context.Draw.Commands.TryDequeue(out command)) @@ -228,23 +306,33 @@ void main() GL.BufferData(BufferTarget.ArrayBuffer, gen.VertexCount * QuikVertex.Stride, ref gen.VertexBuffer[0], BufferUsageHint.StreamDraw); GL.BufferData(BufferTarget.ElementArrayBuffer, gen.ElementCount * 2, ref gen.ElementBuffer[0], BufferUsageHint.StreamDraw); + GL.Enable(EnableCap.Blend); + GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); foreach (QuikDrawCall call in gen.DrawCalls) { GL.BindTexture( TextureTarget.Texture2D, call.Texture == null ? whiteTexture : (call.Texture as OpenGLTexture).TextureId); + if (call.Texture != null) + GL.Uniform2(offsetLoc, 0.5f / call.Texture.Width, 0.5f / call.Texture.Height); GL.DrawElements(BeginMode.Triangles, call.Count, DrawElementsType.UnsignedShort, call.Offset); } + GL.Disable(EnableCap.Blend); int callCount = gen.DrawCalls.Count; gen.Clear(); + System.Threading.Thread.Sleep(1); + window.Context.SwapBuffers(); - if (++i % 60 == 0) + frames++; + float ms = stopwatch.ElapsedMilliseconds; + if (ms - lastMs > 1000) { + Console.WriteLine("Frames: {0}", frames*(ms - lastMs)/1000); + frames = 0; + lastMs = ms; Console.WriteLine("Vertex Usage: {0} ; Element Usage: {1} Calls: {2}", gen.VertexUsage, gen.ElementUsage, callCount); } - - window.Context.SwapBuffers(); } } diff --git a/QuikTestApplication/QuikTestApplication.csproj b/QuikTestApplication/QuikTestApplication.csproj index 0c7591c..5ff706b 100644 --- a/QuikTestApplication/QuikTestApplication.csproj +++ b/QuikTestApplication/QuikTestApplication.csproj @@ -7,6 +7,7 @@ + diff --git a/QuikTestApplication/TestFont.cs b/QuikTestApplication/TestFont.cs index e2f051b..1470e72 100644 --- a/QuikTestApplication/TestFont.cs +++ b/QuikTestApplication/TestFont.cs @@ -22,6 +22,10 @@ namespace QuikTestApplication public IQuikTexture Texture { get; } + public override float Ascender => throw new NotImplementedException(); + + public override float Descender => throw new NotImplementedException(); + public TestFont(QuikContext context) { Texture = context.TextureManager.CreateTexture(TextureSize, false, QuikImageFormat.RgbaU8);