commit 156021347f8d9a7f67b59161d60fd784189be2a9 Author: IllusionMan1212 Date: Fri Jan 19 23:55:07 2024 +0200 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..37bc568 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +o2048 +ols.json diff --git a/assets/fonts/Roboto/LICENSE.txt b/assets/fonts/Roboto/LICENSE.txt new file mode 100644 index 0000000..75b5248 --- /dev/null +++ b/assets/fonts/Roboto/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/assets/fonts/Roboto/Roboto-Bold.ttf b/assets/fonts/Roboto/Roboto-Bold.ttf new file mode 100644 index 0000000..43da14d Binary files /dev/null and b/assets/fonts/Roboto/Roboto-Bold.ttf differ diff --git a/assets/fonts/Rubik/OFL.txt b/assets/fonts/Rubik/OFL.txt new file mode 100644 index 0000000..cdb3773 --- /dev/null +++ b/assets/fonts/Rubik/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/fonts/Rubik/Rubik-VariableFont_wght.ttf b/assets/fonts/Rubik/Rubik-VariableFont_wght.ttf new file mode 100644 index 0000000..bbab349 Binary files /dev/null and b/assets/fonts/Rubik/Rubik-VariableFont_wght.ttf differ diff --git a/assets/icon-128x128.png b/assets/icon-128x128.png new file mode 100644 index 0000000..39cbc6e Binary files /dev/null and b/assets/icon-128x128.png differ diff --git a/assets/icons/close.png b/assets/icons/close.png new file mode 100644 index 0000000..84ee0f7 Binary files /dev/null and b/assets/icons/close.png differ diff --git a/assets/icons/help.png b/assets/icons/help.png new file mode 100644 index 0000000..d8911ff Binary files /dev/null and b/assets/icons/help.png differ diff --git a/assets/icons/settings.png b/assets/icons/settings.png new file mode 100644 index 0000000..997741a Binary files /dev/null and b/assets/icons/settings.png differ diff --git a/freetype/types.odin b/freetype/types.odin new file mode 100644 index 0000000..f8a2d20 --- /dev/null +++ b/freetype/types.odin @@ -0,0 +1,314 @@ +package freetype + +FT_Error :: i32 +@private +FT_Short :: i16 +@private +FT_UShort :: u16 +@private +FT_Int :: i32 +@private +FT_UInt :: u32 +@private +FT_Long :: i64 +@private +FT_ULong :: u64 +@private +FT_String :: u8 + +@private +FT_Pos :: i64 +@private +FT_Fixed :: i64 +@private +FT_SubGlyph :: ^u32 // internal object +@private +FT_Slot_Internal :: ^u32 // opaque handle to internal slot structure +@private +FT_Size_Internal :: ^u32 // opaque handle to internal size structure +@private +FT_Driver :: ^u32 // a handle to a given FreeType font driver object +@private +FT_Memory :: ^u32 // a handle to a given memory manager object + +Library :: ^u32 // handle to a library object + +@private +FT_Render_Mode_ :: enum { + NORMAL = 0, + LIGHT, + MONO, + LCD, + LCD_V, + SDF, + + MAX +} + +LoadFlags :: enum { + DEFAULT = 0x0, + NO_SCALE = 1 << 0, + NO_HINTING = 1 << 1, + RENDER = 1 << 2, + NO_BITMAP = 1 << 3, + VERTICAL_LAYOUT = 1 << 4, + FORCE_AUTOHINT = 1 << 5, + CROP_BITMAP = 1 << 6, + PEDANTIC = 1 << 7, + IGNORE_GLOBAL_ADVANCE_WIDTH = 1 << 9, + NO_RECURSE = 1 << 10, + IGNORE_TRANSFORM = 1 << 11, + MONOCHROME = 1 << 12, + LINEAR_DESIGN = 1 << 13, + NO_AUTOHINT = 1 << 15, + // Bits 16-19 are used by `FT_LOAD_TARGET_` + COLOR = 1 << 20, + COMPUTE_METRICS = 1 << 21, + BITMAP_METRICS_ONLY = 1 << 22, + + /* used internally only by certain font drivers */ + ADVANCE_ONLY = 1 << 8, + SBITS_ONLY = 1 << 14, + + FT_LOAD_TARGET_NORMAL = (int(FT_Render_Mode_.NORMAL) & 15) << 16, + FT_LOAD_TARGET_LIGHT = (int(FT_Render_Mode_.LIGHT) & 15) << 16, + FT_LOAD_TARGET_MONO = (int(FT_Render_Mode_.MONO) & 15) << 16, + FT_LOAD_TARGET_LCD = (int(FT_Render_Mode_.LCD) & 15) << 16, + FT_LOAD_TARGET_LCD_V = (int(FT_Render_Mode_.LCD_V) & 15) << 16, +} + +@private +FT_Encoding :: enum { + // TODO: fill this out +} + +@private +FT_CharMapRec_ :: struct { + face: Face, + encoding: FT_Encoding, + platform_id: FT_UShort, + encoding_id: FT_UShort, +} + +FT_CharMap :: ^FT_CharMapRec_ + +@private +FT_Bitmap_Size :: struct { + height: FT_Short, + width: FT_Short, + + size: FT_Pos, + + x_ppem: FT_Pos, + y_ppem: FT_Pos, +} + +@private +FT_Generic :: struct { + data: rawptr, + finalizer: ^proc(object: rawptr), +} + +@private +FT_BBox :: struct { + xMin, yMin: FT_Pos, + xMax, yMax: FT_Pos, +} + +@private +FT_Vector :: struct { + x, y: FT_Pos, +} + +@private +FT_Glyph_Metrics :: struct { + width: FT_Pos, + height: FT_Pos, + + horiBearingX: FT_Pos, + horiBearingY: FT_Pos, + horiAdvance: FT_Pos, + + vertBearingX: FT_Pos, + vertBearingY: FT_Pos, + vertAdvance: FT_Pos, +} + +@private +FT_Outline :: struct { + n_contours: i16, + n_points: i16, + + points: ^FT_Vector, + tags: ^u8, + contours: ^i16, + + flags: i32, +} + +@private +FT_GlyphSlotRec_ :: struct { + library: Library, + face: Face, + next: FT_GlyphSlot, + glyph_index: FT_UInt, /* new in 2.10; was reserved previously */ + generic: FT_Generic, + + metrics: FT_Glyph_Metrics, + linearHoriAdvance: FT_Fixed, + linearVertAdvance: FT_Fixed, + advance: FT_Vector, + + format: u32 /*FT_Glyph_Format*/, // stub this to u32 for now since I can't be bothered to define the enum + + bitmap: Bitmap, + bitmap_left: FT_Int, + bitmap_top: FT_Int, + + outline: FT_Outline, + + num_subglyphs: FT_UInt, + subglyphs: FT_SubGlyph, + + control_data: rawptr, + control_len: i64, + + lsb_delta: FT_Pos, + rsb_delta: FT_Pos, + + other: rawptr, + + internal: FT_Slot_Internal, +} + +FT_GlyphSlot :: ^FT_GlyphSlotRec_ + +@private +FT_Size_Metrics :: struct { + x_ppem: FT_UShort, /* horizontal pixels per EM */ + y_ppem: FT_UShort, /* vertical pixels per EM */ + + x_scale: FT_Fixed, /* scaling values used to convert font */ + y_scale: FT_Fixed, /* units to 26.6 fractional pixels */ + + ascender: FT_Pos, /* ascender in 26.6 frac. pixels */ + descender: FT_Pos, /* descender in 26.6 frac. pixels */ + height: FT_Pos, /* text height in 26.6 frac. pixels */ + max_advance: FT_Pos, /* max horizontal advance, in 26.6 pixels */ +} + +@private +FT_SizeRec :: struct { + face: Face, + generic: FT_Generic, + metrics: FT_Size_Metrics, + internal: FT_Size_Internal, +} + +@private +FT_Size :: ^FT_SizeRec + +//@private +//FT_StreamRec_ :: struct { +// unsigned char* base, +// size: u64, +// pos: u64, +// +// FT_StreamDesc descriptor, +// FT_StreamDesc pathname, +// FT_Stream_IoFunc read, +// FT_Stream_CloseFunc close, +// +// FT_Memory memory, +// unsigned char* cursor, +// unsigned char* limit, +//} + +@private +FT_ListNodeRec_ :: struct { + prev: FT_ListNode, + next: FT_ListNode, + data: rawptr, +} + +@private +FT_ListNode :: ^FT_ListNodeRec_ + +@private +FT_ListRec :: struct { + head: FT_ListNode, + tail: FT_ListNode, +} + +@private +FT_FaceRec_ :: struct { + num_faces: FT_Long, + face_index: FT_Long, + + face_flags: FT_Long, + style_flags: FT_Long, + + num_glyphs: FT_Long, + + family_name: ^FT_String, + style_name: ^FT_String, + + num_fixed_sizes: FT_Int, + available_sizes: ^FT_Bitmap_Size, + + num_charmaps: FT_Int, + charmaps: ^FT_CharMap, + + generic: FT_Generic, + + /*# The following member variables (down to `underline_thickness`) */ + /*# are only relevant to scalable outlines; cf. @FT_Bitmap_Size */ + /*# for bitmap fonts. */ + bbox: FT_BBox, + + units_per_EM: FT_UShort, + ascender: FT_Short, + descender: FT_Short, + height: FT_Short, + + max_advance_width: FT_Short, + max_advance_height: FT_Short, + + underline_position: FT_Short, + underline_thickness: FT_Short, + + glyph: FT_GlyphSlot, + size: FT_Size, + charmap: FT_CharMap, + + /*@private begin */ + driver: FT_Driver, + memory: FT_Memory, + stream: /*FT_Stream*/ ^u32, // TODO: stub it out for now + + sizes_list: FT_ListRec, + + autohint: FT_Generic, // face-specific auto-hinter data + extensions: rawptr, // unused + + internal: /*FT_Face_Internal*/ ^u32, // TODO: stub it out for now + + /*@private end */ + + //@private + //@packed + //@align(8) +} + +Bitmap :: struct { + rows: u32, + width: u32, + pitch: i32, + buffer: [^]u8, + num_grays: u16, + pixel_mode: u8, + palette_mode: u8, + palette: rawptr, +} + +Face :: ^FT_FaceRec_ diff --git a/freetype/wrapper.odin b/freetype/wrapper.odin new file mode 100644 index 0000000..d92f0a1 --- /dev/null +++ b/freetype/wrapper.odin @@ -0,0 +1,18 @@ +package freetype + +foreign import freetype "system:freetype" + +foreign freetype { + @(link_name = "FT_Init_FreeType") + Init_FreeType :: proc(lib: ^Library) -> FT_Error --- + @(link_name = "FT_New_Face") + New_Face :: proc(lib: Library, file_pathname: cstring, face_index: FT_Long, face: ^Face) -> FT_Error --- + @(link_name = "FT_Set_Pixel_Sizes") + Set_Pixel_Sizes :: proc(face: Face, pixel_width: FT_UInt, pixel_height: FT_UInt) -> FT_Error --- + @(link_name = "FT_Load_Char") + Load_Char :: proc(face: Face, char_code: FT_ULong, load_flags: LoadFlags) -> FT_Error --- + @(link_name = "FT_Done_Face") + Done_Face :: proc(face: Face) -> FT_Error --- + @(link_name = "FT_Done_FreeType") + Done_FreeType :: proc(lib: Library) -> FT_Error --- +} diff --git a/game.odin b/game.odin new file mode 100644 index 0000000..4d1b6f3 --- /dev/null +++ b/game.odin @@ -0,0 +1,1177 @@ +package main + +import "core:fmt" +import "core:math" +import "core:math/rand" +import "core:time" + +import "zephr" + +Icon :: enum { + HELP_ICON, + SETTINGS_ICON, + CLOSE_ICON, +} + +MoveDir :: enum { + UP, + DOWN, + LEFT, + RIGHT, +} + +ColorPalette :: struct { + bg_color: zephr.Color, + board_bg_color: zephr.Color, + tile_colors: [11]zephr.Color, +} + +Tile :: struct { + value: u16, + new_value: u16, + merged: bool, + + anim_x_offset_relative: f64, + anim_y_offset_relative: f64, + tiles_to_move: u8, +} + +Game :: struct { + board: [4][4]Tile, + score: u32, + last_move_dir: MoveDir, + animating: bool, + spawning_new_tile: bool, + has_lost: bool, + game_over_bg_opacity: f32, + game_over_opacity: f32, + spawning_tile_scale: f32, + spawning_tile_coords: zephr.Vec2, + spawning_tile_value: u16, + + quit_dialog: bool, + settings_dialog: bool, + help_dialog: bool, + + icon_textures: [Icon]zephr.TextureId, + palette: ColorPalette, +} + +@private +TILE_ANIM_SPEED :: 2.5 +@private +TILE_SPAWN_SPEED :: 6.0 +@private +GAMEOVER_ANIM_DURATION :: time.Millisecond * 300 + +@private +neighbors := [4]zephr.Vec2 { + {-1, 0}, // left + {1, 0}, // right + {0, -1}, // up + {0, 1}, // down +} + +// nocheckin +timeElapsed: time.Duration = 0 + +@private +game : Game + +get_tile_color :: proc(value: u16) -> zephr.Color { + switch (value) { + case 2: + return game.palette.tile_colors[0]; + case 4: + return game.palette.tile_colors[1]; + case 8: + return game.palette.tile_colors[2]; + case 16: + return game.palette.tile_colors[3]; + case 32: + return game.palette.tile_colors[4]; + case 64: + return game.palette.tile_colors[5]; + case 128: + return game.palette.tile_colors[6]; + case 256: + return game.palette.tile_colors[7]; + case 512: + return game.palette.tile_colors[8]; + case 1024: + return game.palette.tile_colors[9]; + case 2048: + return game.palette.tile_colors[10]; + case: + return game.palette.tile_colors[10]; + } +} + +get_tile_font_size :: proc(value: u16) -> u32 { + switch (value) { + case 2: fallthrough + case 4: fallthrough + case 8: + return 60; + case 16: fallthrough + case 32: fallthrough + case 64: + return 55; + case 128: fallthrough + case 256: fallthrough + case 512: + return 50; + case 1024: fallthrough + case 2048: + return 45; + case: + return 40; + } +} + +///////////////////////////// +// +// +// Game Logic +// +// +///////////////////////////// + + +move_right :: proc() { + game.last_move_dir = .RIGHT; + + for i in 0..<4 { + for j := 2; j >= 0; j -= 1 { + if (game.board[i][j].value == 0) { + continue; + } + tiles_to_move := 0; + x := j; + + src := &game.board[i][j]; + + loop: for { + if (x >= 3) do break loop + x += 1 + + // next tile is empty so we can move there + if (game.board[i][x].new_value == 0) { + tiles_to_move += 1; + continue; + } + + // next tile has the same value so we can merge and move + if (game.board[i][x].new_value == src.new_value && !game.board[i][x].merged) { + tiles_to_move += 1; + game.board[i][x].merged = true; + break; + } else { + // next tile has a different value so we can't move there + x -= 1; + break; + } + } + + src.tiles_to_move = auto_cast tiles_to_move; + + if (tiles_to_move > 0) { + if (game.board[i][x].merged) { + game.board[i][x].new_value *= 2; + game.score += auto_cast game.board[i][x].new_value; + } else { + game.board[i][x].new_value = src.new_value; + } + src.new_value = 0; + game.animating = true; + game.spawning_tile_coords = get_random_available_tile_coords(); + game.spawning_tile_value = get_spawned_tile_value(); + } + } + } +} + +// TODO: ideas for refactoring the animation logic and separating game state from rendering +// the board is just a state that we keep in memory but never use it for rendering +// and a tile is an enitity that has a position (in the board or on the screen??) that we can animate using lerp +// we draw the tile entities and not the board state. the tile entities update their positions according to the board state at the end of the frame?? +move_left :: proc() { + game.last_move_dir = .LEFT; + + for i in 0..<4 { + for j in 1..<4 { + if (game.board[i][j].value == 0) { + continue; + } + tiles_to_move := 0; + x := j; + + src := &game.board[i][j]; + + loop: for { + if (x <= 0) do break loop + x -= 1; + + // next tile is empty so we can move there + if (game.board[i][x].new_value == 0) { + tiles_to_move += 1; + continue; + } + + // next tile has the same value so we can merge and move + if (game.board[i][x].new_value == src.new_value && !game.board[i][x].merged) { + tiles_to_move += 1; + game.board[i][x].merged = true; + break; + } else { + // next tile has a different value so we can't move there + x += 1 + break; + } + } + + src.tiles_to_move = auto_cast tiles_to_move; + + if (tiles_to_move > 0) { + if (game.board[i][x].merged) { + game.board[i][x].new_value *= 2; + game.score += auto_cast game.board[i][x].new_value; + } else { + game.board[i][x].new_value = src.new_value; + } + src.new_value = 0; + game.animating = true; + game.spawning_tile_coords = get_random_available_tile_coords(); + game.spawning_tile_value = get_spawned_tile_value(); + } + } + } +} + +print_board :: proc() { + for i in 0..<4 { + for j in 0..<4 { + fmt.print(game.board[i][j].value, " "); + } + fmt.println(); + } + fmt.println(); +} + +print_new_board :: proc() { + for i in 0..<4 { + for j in 0..<4 { + fmt.print(game.board[i][j].new_value, " "); + } + fmt.println(); + } + fmt.println(); +} + +move_down :: proc() { + game.last_move_dir = .DOWN; + + for i := 2; i >= 0; i -= 1 { + for j in 0..<4 { + if (game.board[i][j].value == 0) { + continue; + } + tiles_to_move := 0; + y := i; + + src := &game.board[i][j]; + + loop: for { + if (y >= 3) do break loop + y += 1; + + if (game.board[y][j].new_value == 0) { + tiles_to_move += 1; + continue; + } + + if (game.board[y][j].new_value == src.new_value && !game.board[y][j].merged) { + tiles_to_move += 1; + game.board[y][j].merged = true; + break; + } else { + y -= 1; + break; + } + } + + src.tiles_to_move = auto_cast tiles_to_move; + + if (tiles_to_move > 0) { + if (game.board[y][j].merged) { + game.board[y][j].new_value *= 2; + game.score += auto_cast game.board[y][j].new_value; + } else { + game.board[y][j].new_value = src.new_value; + } + src.new_value = 0; + game.animating = true; + game.spawning_tile_coords = get_random_available_tile_coords(); + game.spawning_tile_value = get_spawned_tile_value(); + } + } + } +} + +move_up :: proc() { + game.last_move_dir = .UP; + + for i in 1..<4 { + for j in 0..<4 { + if (game.board[i][j].new_value == 0) { + continue; + } + tiles_to_move := 0; + y := i; + + src := &game.board[i][j]; + + loop: for { + if (y <= 0) do break loop + y -= 1; + + if (game.board[y][j].new_value == 0) { + tiles_to_move += 1; + continue; + } + + if (game.board[y][j].new_value == src.new_value && !game.board[y][j].merged) { + tiles_to_move += 1; + game.board[y][j].merged = true; + break; + } else { + y += 1; + break; + } + } + + src.tiles_to_move = auto_cast tiles_to_move; + + if (tiles_to_move > 0) { + if (game.board[y][j].merged) { + game.board[y][j].new_value *= 2; + game.score += auto_cast game.board[y][j].new_value; + } else { + game.board[y][j].new_value = src.new_value; + } + src.new_value = 0; + game.animating = true; + game.spawning_tile_coords = get_random_available_tile_coords(); + game.spawning_tile_value = get_spawned_tile_value(); + } + } + } +} + +reset_palette :: proc() { + game.palette = ColorPalette{ + bg_color = zephr.Color{248, 250, 230, 255}, + board_bg_color = zephr.Color{140, 154, 147, 255}, + + tile_colors = [11]zephr.Color{ + zephr.Color{238, 228, 218, 255}, // 2 + zephr.Color{237, 224, 200, 255}, // 4 + zephr.Color{242, 177, 121, 255}, // 8 + zephr.Color{245, 149, 99, 255}, // 16 + zephr.Color{246, 124, 95, 255}, // 32 + zephr.Color{246, 94, 59, 255}, // 64 + zephr.Color{237, 207, 114, 255}, // 128 + zephr.Color{237, 204, 97, 255}, // 256 + zephr.Color{237, 200, 80, 255}, // 512 + zephr.Color{237, 197, 63, 255}, // 1024 + zephr.Color{237, 194, 46, 255}, // 2048 + }, + } +} + +reset_game :: proc() { + timeElapsed = 0 + + game.score = 0; + game.animating = false; + game.spawning_new_tile = false; + game.quit_dialog = false; + game.help_dialog = false; + game.settings_dialog = false; + game.has_lost = false; + game.game_over_bg_opacity = 0; + game.game_over_opacity = 0; + + for y in 0..<4 { + for x in 0..<4 { + game.board[y][x].value = 0; + game.board[y][x].new_value = 0; + game.board[y][x].merged = false; + game.board[y][x].tiles_to_move = 0; + game.board[y][x].anim_x_offset_relative = 0; + game.board[y][x].anim_y_offset_relative = 0; + } + } + + for i in 0..<2 { + spawn_random_tile(); + } +} + +game_attempt_quit :: proc() { + game.help_dialog = false; + game.settings_dialog = false; + game.quit_dialog = true; +} + +game_init :: proc() { + game.icon_textures[.HELP_ICON] = zephr.load_texture("assets/icons/help.png") + game.icon_textures[.SETTINGS_ICON] = zephr.load_texture("assets/icons/settings.png") + game.icon_textures[.CLOSE_ICON] = zephr.load_texture("assets/icons/close.png") + + reset_palette() + + for i in 0..<2 { + spawn_random_tile() + } +} + +get_spawned_tile_value :: proc() -> u16 { + // 90% chance of spawning a 2, 10% chance of spawning a 4 + if (rand.int_max(11) < 9) { + return 2; + } else { + return 4; + } +} + +spawn_new_tile_with_value :: proc(x, y: u8, value: u16) { + game.board[x][y].value = value; + game.board[x][y].new_value = value; +} + +spawn_new_tile :: proc(x, y: u8) { + val := get_spawned_tile_value(); + + game.board[x][y].value = val; + game.board[x][y].new_value = val; +} + +get_random_available_tile_coords :: proc() -> zephr.Vec2 { + available_tiles_count := 0; + available_tiles: [16]int + + for i in 0..<4 { + for j in 0..<4 { + if (game.board[i][j].new_value == 0 && !game.board[i][j].merged) { + available_tiles[available_tiles_count] = i * 4 + j; + available_tiles_count += 1; + } + } + } + + rand_idx := rand.int_max(available_tiles_count) + + x := available_tiles[rand_idx] / 4; + y := available_tiles[rand_idx] % 4; + + return zephr.Vec2{cast(f32)x, cast(f32)y}; +} + +spawn_random_tile :: proc() { + coords := get_random_available_tile_coords(); + spawn_new_tile(cast(u8)coords.x, cast(u8)coords.y); +} + +gameover :: proc() -> bool { + if (game.has_lost) do return false + + for y in 0..<4 { + for x in 0..<4 { + if (game.board[y][x].value == 0) { + return false; + } + + for i in 0..<4 { + new_y := cast(int)clamp(cast(f32)y + neighbors[i].y, 0, 3); + new_x := cast(int)clamp(cast(f32)x + neighbors[i].x, 0, 3); + + if ((new_y != y || new_x != x) && game.board[y][x].value == game.board[new_y][new_x].value) { + return false; + } + } + } + } + + + return true; +} + +handle_keyboard_input :: proc(e: zephr.Event) { + can_move := !game.quit_dialog && !game.help_dialog && !game.settings_dialog && !game.animating; + + if (.LEFT_CTRL in e.key.mods && e.key.scancode == .Q) { + game_attempt_quit(); + } else if (e.key.scancode == .ESCAPE) { + game.quit_dialog = false; + game.help_dialog = false; + game.settings_dialog = false; + } else if (e.key.scancode == .F11) { + zephr.toggle_fullscreen(); + } else if (e.key.scancode == .UP) { + if (can_move) do move_up() + } else if (e.key.scancode == .DOWN) { + if (can_move) do move_down() + } else if (e.key.scancode == .LEFT) { + if (can_move) do move_left() + } else if (e.key.scancode == .RIGHT) { + if (can_move) do move_right() + } +} + +game_loop :: proc() { + game_init() + + last_frame: time.Duration = 0 + + for (!zephr.should_quit()) { + e: zephr.Event + + for (zephr.iter_events(&e)) { + #partial switch (e.type) { + case .WINDOW_CLOSED: + if (game.quit_dialog) { + zephr.quit(); + } else { + game_attempt_quit(); + } + case .KEY_PRESSED: + handle_keyboard_input(e); + } + } + + now := zephr.get_time() + delta_t := now - last_frame + last_frame = now; + + update_positions(delta_t) + + draw_bg(); + draw_board(); + draw_ui(); + + zephr.swap_buffers() + } +} + +update_positions :: proc(delta_t: time.Duration) { + // TODO: lerp the positions and use delta_t for it + delta_t_f := time.duration_seconds(delta_t) + // TODO: experiment; I think I can lerp the tile positions instead of doing this + + if (game.animating) { + switch (game.last_move_dir) { + case .LEFT: + for i in 0..<4 { + for j in 1..<4 { + tiles_to_move := game.board[i][j].tiles_to_move; + speed := TILE_ANIM_SPEED * cast(f64)tiles_to_move; + src := &game.board[i][j]; + dest := &game.board[i][j - cast(int)tiles_to_move]; + + if (src.tiles_to_move != 0) { + src.anim_x_offset_relative -= speed * delta_t_f; + + if (src.anim_x_offset_relative <= -0.240 * (cast(f64)tiles_to_move + (cast(f64)tiles_to_move * 0.02))) { + if (dest.new_value == src.value * 2) { + dest.merged = false; + } + + src.value = src.new_value; + dest.value = dest.new_value; + + src.tiles_to_move = 0; + src.anim_x_offset_relative = 0; + game.spawning_new_tile = true; + } + } + } + } + case .RIGHT: + for i in 0..<4 { + for j := 2; j >= 0; j -= 1 { + tiles_to_move := game.board[i][j].tiles_to_move; + speed := TILE_ANIM_SPEED * cast(f64)tiles_to_move; + src := &game.board[i][j]; + dest := &game.board[i][j + cast(int)tiles_to_move]; + + if (src.tiles_to_move != 0) { + src.anim_x_offset_relative += speed * delta_t_f; + + if (src.anim_x_offset_relative >= 0.240 * (cast(f64)tiles_to_move + (cast(f64)tiles_to_move * 0.02))) { + if (dest.new_value == src.value * 2) { + dest.merged = false; + } + + src.value = src.new_value; + dest.value = dest.new_value; + + src.tiles_to_move = 0; + src.anim_x_offset_relative = 0; + game.spawning_new_tile = true; + } + } + } + } + case .UP: + for i in 1..<4 { + for j in 0..<4 { + tiles_to_move := game.board[i][j].tiles_to_move; + speed := TILE_ANIM_SPEED * cast(f64)tiles_to_move; + src := &game.board[i][j]; + dest := &game.board[i - cast(int)tiles_to_move][j]; + + if (src.tiles_to_move != 0) { + src.anim_y_offset_relative -= speed * delta_t_f; + + if (src.anim_y_offset_relative <= -0.240 * (cast(f64)tiles_to_move + (cast(f64)tiles_to_move * 0.02))) { + if (dest.new_value == src.value * 2) { + dest.merged = false; + } + + src.value = src.new_value; + dest.value = dest.new_value; + + src.tiles_to_move = 0; + src.anim_y_offset_relative = 0; + game.spawning_new_tile = true; + } + } + } + } + case .DOWN: + for i := 2; i >= 0; i -= 1 { + for j in 0..<4 { + tiles_to_move := game.board[i][j].tiles_to_move; + speed := TILE_ANIM_SPEED * cast(f64)tiles_to_move; + src := &game.board[i][j]; + dest := &game.board[i + cast(int)tiles_to_move][j]; + + if (src.tiles_to_move != 0) { + src.anim_y_offset_relative += speed * delta_t_f; + + if (src.anim_y_offset_relative >= 0.240 * (cast(f64)tiles_to_move + (cast(f64)tiles_to_move * 0.02))) { + if (dest.new_value == src.value * 2) { + dest.merged = false; + } + + src.value = src.new_value; + dest.value = dest.new_value; + + src.tiles_to_move = 0; + src.anim_y_offset_relative = 0; + game.spawning_new_tile = true; + } + } + } + } + } + } + + if (gameover()) { + game.has_lost = true; + game.animating = true; + } + + if (game.spawning_new_tile) { + game.spawning_tile_scale += (TILE_SPAWN_SPEED * cast(f32)delta_t_f); + + if (game.spawning_tile_scale >= 1.0) { + spawn_new_tile_with_value(cast(u8)game.spawning_tile_coords.x, cast(u8)game.spawning_tile_coords.y, game.spawning_tile_value); + game.spawning_tile_coords = zephr.Vec2{0, 0}; + game.spawning_tile_value = 0; + game.spawning_new_tile = false; + game.animating = false; + game.spawning_tile_scale = 0.0 + } + } + + if (game.has_lost) { + if timeElapsed < GAMEOVER_ANIM_DURATION { + game.game_over_opacity = cast(f32)math.lerp(0.0, 255.0, cast(f64)timeElapsed / cast(f64)GAMEOVER_ANIM_DURATION) + game.game_over_bg_opacity = cast(f32)math.lerp(0.0, 200.0, cast(f64)timeElapsed / cast(f64)GAMEOVER_ANIM_DURATION) + timeElapsed += delta_t + } else { + game.game_over_bg_opacity = 255 + game.game_over_bg_opacity = 200 + } + } +} + + +/////////////////////////////////// +// +// +// Drawing +// +// +/////////////////////////////////// + + +draw_bg :: proc() { + window_size := zephr.get_window_size(); + board_con := zephr.DEFAULT_UI_CONSTRAINTS + + zephr.set_width_constraint(&board_con, window_size.x, .FIXED); + zephr.set_height_constraint(&board_con, window_size.y, .FIXED); + + // bg + style := zephr.UiStyle{ + bg_color = game.palette.bg_color, + align = .TOP_LEFT, + }; + zephr.draw_quad(&board_con, style); + + text_batch: zephr.GlyphInstanceList + reserve(&text_batch, 32) + + // 2048 text + text_con := zephr.DEFAULT_UI_CONSTRAINTS; + + zephr.set_y_constraint(&text_con, 0.05, .RELATIVE); + zephr.set_width_constraint(&text_con, 1.5, .RELATIVE_PIXELS); + + zephr.add_text_instance(&text_batch, "2048", 100, text_con, zephr.Color{34, 234, 82, 155}, .TOP_CENTER); + + // score + buf: [24]byte + score := fmt.bprintf(buf[:], "Score %d", game.score); + + zephr.set_parent_constraint(&text_con, nil); + zephr.set_x_constraint(&text_con, 0.05, .RELATIVE); + zephr.set_y_constraint(&text_con, 0.8, .RELATIVE); + zephr.add_text_instance(&text_batch, score, 40, text_con, zephr.COLOR_BLACK, .TOP_LEFT); + + zephr.draw_text_batch(&text_batch); +} + +draw_board :: proc() { + // board + board_con := zephr.DEFAULT_UI_CONSTRAINTS; + zephr.set_y_constraint(&board_con, 0.05, .RELATIVE); + zephr.set_height_constraint(&board_con, 0.6, .RELATIVE); + zephr.set_width_constraint(&board_con, 1, .ASPECT_RATIO); + style := zephr.UiStyle{ + bg_color = game.palette.board_bg_color, + border_radius = board_con.width * 0.02, + align = .CENTER, + }; + zephr.draw_quad(&board_con, style); + + // empty tiles + tile_padding := board_con.width * 0.02; + tile_height := board_con.height * 0.225; + tile_board_radius := tile_height * 0.15 + + empty_tile_con := zephr.DEFAULT_UI_CONSTRAINTS; + zephr.set_parent_constraint(&empty_tile_con, &board_con); + zephr.set_height_constraint(&empty_tile_con, tile_height, .FIXED); + zephr.set_width_constraint(&empty_tile_con, 1, .ASPECT_RATIO); + + style = zephr.UiStyle{ + bg_color = zephr.mult_color(game.palette.board_bg_color, 0.85), + border_radius = tile_board_radius, + align = .TOP_LEFT, + }; + for i in 0..<4 { + zephr.set_y_constraint(&empty_tile_con, (tile_height + tile_padding) * cast(f32)i + tile_padding, .FIXED); + for j in 0..<4 { + zephr.set_x_constraint(&empty_tile_con, (tile_height + tile_padding) * cast(f32)j + tile_padding, .FIXED); + zephr.draw_quad(&empty_tile_con, style); + } + } + + // filled tiles + text_con := zephr.DEFAULT_UI_CONSTRAINTS; + zephr.set_width_constraint(&text_con, 1.5, .RELATIVE_PIXELS); + tile_con := zephr.DEFAULT_UI_CONSTRAINTS; + zephr.set_parent_constraint(&tile_con, &board_con); + zephr.set_height_constraint(&tile_con, tile_height, .FIXED); + zephr.set_width_constraint(&tile_con, 1, .ASPECT_RATIO); + + for i in 0..<4 { + for j in 0..<4 { + tile := &game.board[i][j]; + tile_color := get_tile_color(tile.value); + style := zephr.UiStyle { + bg_color = tile_color, + border_radius = tile_board_radius, + align = .TOP_LEFT, + } + buf: [16]byte + value := fmt.bprintf(buf[:], "%d", tile.value) + tile_y_pos := ((tile_height + tile_padding) * cast(f32)i + tile_padding + cast(f32)game.board[i][j].anim_y_offset_relative * board_con.height); + tile_x_pos := ((tile_height + tile_padding) * cast(f32)j + tile_padding + cast(f32)game.board[i][j].anim_x_offset_relative * board_con.height); + + zephr.set_y_constraint(&tile_con, tile_y_pos, .FIXED); + zephr.set_x_constraint(&tile_con, tile_x_pos, .FIXED); + if (tile.value > 0) { + zephr.draw_quad(&tile_con, style); + + // value text + zephr.set_parent_constraint(&text_con, &tile_con); + zephr.set_x_constraint(&text_con, 0, .FIXED); + zephr.set_y_constraint(&text_con, 0, .FIXED); + + zephr.draw_text(value, get_tile_font_size(tile.value), text_con, zephr.mult_color(tile_color, 0.5), .CENTER); + } + } + } + + if (game.spawning_new_tile) { + // spawning tile + tile_color := get_tile_color(game.spawning_tile_value); + style := zephr.UiStyle { + bg_color = tile_color, + border_radius = tile_board_radius * game.spawning_tile_scale, + align = .TOP_LEFT, + }; + tile_x_pos := ((tile_height + tile_padding) * game.spawning_tile_coords.y + tile_padding) + tile_height / 2 - game.spawning_tile_scale * tile_height / 2; + tile_y_pos := ((tile_height + tile_padding) * game.spawning_tile_coords.x + tile_padding) + tile_height / 2 - game.spawning_tile_scale * tile_height / 2; + zephr.set_x_constraint(&tile_con, tile_x_pos, .FIXED); + zephr.set_y_constraint(&tile_con, tile_y_pos, .FIXED); + zephr.set_height_constraint(&tile_con, tile_height * game.spawning_tile_scale, .FIXED); + zephr.set_width_constraint(&tile_con, 1, .ASPECT_RATIO); + zephr.draw_quad(&tile_con, style); + // value text + zephr.set_parent_constraint(&text_con, &tile_con); + zephr.set_x_constraint(&text_con, 0, .FIXED); + zephr.set_y_constraint(&text_con, 0, .FIXED); + + buf: [16]byte + value := fmt.bprintf(buf[:], "%d", game.spawning_tile_value) + zephr.draw_text(value, cast(u32)(cast(f32)get_tile_font_size(game.spawning_tile_value) * game.spawning_tile_scale), text_con, zephr.mult_color(tile_color, 0.5), .CENTER); + } +} + +draw_ui :: proc() { + bg_btns_state: zephr.ButtonState = (game.quit_dialog || game.help_dialog || game.settings_dialog || game.has_lost) ? .INACTIVE : .ACTIVE; + icon_btn_width :: 80; + icon_btn_offset :: 8; + icon_btns_padding :: 10; + + btn_con := zephr.DEFAULT_UI_CONSTRAINTS; + zephr.set_x_constraint(&btn_con, -icon_btn_offset, .RELATIVE_PIXELS); + zephr.set_y_constraint(&btn_con, icon_btn_offset, .RELATIVE_PIXELS); + zephr.set_width_constraint(&btn_con, icon_btn_width, .RELATIVE_PIXELS); + zephr.set_height_constraint(&btn_con, 1, .ASPECT_RATIO); + + style := zephr.UiStyle { + bg_color = zephr.Color{201, 146, 126, 255}, + fg_color = zephr.Color{40, 92, 100, 255}, + border_radius = btn_con.height * 0.15, + align = .TOP_RIGHT, + }; + if (zephr.draw_icon_button(&btn_con, game.icon_textures[.HELP_ICON], style, bg_btns_state)) { + game.help_dialog = true; + } + + zephr.set_x_constraint(&btn_con, -icon_btn_offset - icon_btn_width - icon_btns_padding, .RELATIVE_PIXELS); + if (zephr.draw_icon_button(&btn_con, game.icon_textures[.SETTINGS_ICON], style, bg_btns_state)) { + game.settings_dialog = true; + } + + zephr.set_x_constraint(&btn_con, 0, .RELATIVE_PIXELS); + zephr.set_y_constraint(&btn_con, -0.05, .RELATIVE); + zephr.set_width_constraint(&btn_con, 200, .RELATIVE_PIXELS); + zephr.set_height_constraint(&btn_con, 65, .RELATIVE_PIXELS); + style = zephr.UiStyle { + bg_color = zephr.Color{201, 172, 126, 255}, + fg_color = zephr.COLOR_BLACK, + border_radius = btn_con.height * 0.25, + align = .BOTTOM_CENTER, + }; + if (zephr.draw_button(&btn_con, "New Game", style, bg_btns_state)) { + reset_game(); + } + + if (game.settings_dialog) { + draw_settings_dialog(); + } + + if (game.help_dialog) { + draw_help_dialog(); + } + + if (game.has_lost) { + draw_game_over(); + } + + if (game.quit_dialog) { + draw_quit_dialog(); + } +} + +draw_settings_dialog :: proc() { + dialog_con := zephr.DEFAULT_UI_CONSTRAINTS + zephr.set_width_constraint(&dialog_con, 1, .RELATIVE); + zephr.set_height_constraint(&dialog_con, 1, .RELATIVE); + zephr.set_x_constraint(&dialog_con, 0, .FIXED); + zephr.set_y_constraint(&dialog_con, 0, .FIXED); + style := zephr.UiStyle { + bg_color = zephr.Color{0, 0, 0, 200}, + align = .CENTER, + }; + zephr.draw_quad(&dialog_con, style); + + content_card_con := zephr.DEFAULT_UI_CONSTRAINTS + zephr.set_parent_constraint(&content_card_con, &dialog_con); + zephr.set_width_constraint(&content_card_con, 0.55, .RELATIVE); + zephr.set_height_constraint(&content_card_con, 0.6, .RELATIVE); + style = zephr.UiStyle{ + bg_color = zephr.Color{135, 124, 124, 255}, + border_radius = 8, + align = .CENTER, + }; + zephr.draw_quad(&content_card_con, style); + + text_con := zephr.DEFAULT_UI_CONSTRAINTS + zephr.set_parent_constraint(&text_con, &content_card_con); + zephr.set_width_constraint(&text_con, 1, .RELATIVE_PIXELS); + zephr.set_x_constraint(&text_con, 0, .FIXED); + zephr.set_y_constraint(&text_con, 40, .RELATIVE_PIXELS); + zephr.draw_text("Settings", 64, text_con, zephr.COLOR_YELLOW, .TOP_CENTER); + + zephr.set_x_constraint(&text_con, 30, .RELATIVE_PIXELS); + zephr.set_y_constraint(&text_con, 150, .RELATIVE_PIXELS); + zephr.draw_text("Color Palette", 40, text_con, zephr.COLOR_WHITE, .TOP_LEFT); + + bg_color_text := "Background: "; + font_size: u32 = 30; + zephr.set_x_constraint(&text_con, 30, .RELATIVE_PIXELS); + zephr.set_y_constraint(&text_con, 220, .RELATIVE_PIXELS); + zephr.set_height_constraint(&text_con, 0.9, .RELATIVE); + zephr.draw_text(bg_color_text, font_size, text_con, zephr.COLOR_WHITE, .TOP_LEFT); + + text_size := zephr.calculate_text_size(bg_color_text, font_size); + color_picker_con := zephr.DEFAULT_UI_CONSTRAINTS + zephr.set_parent_constraint(&color_picker_con, &content_card_con); + zephr.set_x_constraint(&color_picker_con, 30 + text_size.x + 20, .RELATIVE_PIXELS); + zephr.set_y_constraint(&color_picker_con, 220 - text_size.y / 2 + 6, .RELATIVE_PIXELS); + zephr.set_width_constraint(&color_picker_con, 100, .RELATIVE_PIXELS); + zephr.set_height_constraint(&color_picker_con, 40, .RELATIVE_PIXELS); + zephr.draw_color_picker(&color_picker_con, &game.palette.bg_color, .TOP_LEFT, .ACTIVE); + + zephr.set_y_constraint(&text_con, 270, .RELATIVE_PIXELS); + zephr.draw_text("Board: ", font_size, text_con, zephr.COLOR_WHITE, .TOP_LEFT); + + zephr.set_x_constraint(&color_picker_con, 30 + text_size.x + 20, .RELATIVE_PIXELS); + zephr.set_y_constraint(&color_picker_con, 270 - text_size.y / 2 + 6, .RELATIVE_PIXELS); + zephr.set_width_constraint(&color_picker_con, 100, .RELATIVE_PIXELS); + zephr.set_height_constraint(&color_picker_con, 40, .RELATIVE_PIXELS); + zephr.draw_color_picker(&color_picker_con, &game.palette.board_bg_color, .TOP_LEFT, .ACTIVE); + + zephr.set_y_constraint(&text_con, 320, .RELATIVE_PIXELS); + zephr.draw_text("Tiles: ", font_size, text_con, zephr.COLOR_WHITE, .TOP_LEFT); + + tile_size: f32 = 60; + for i in 0..<11 { + zephr.set_x_constraint(&color_picker_con, 30 + text_size.x + 20 + (cast(f32)i * (tile_size + 10)), .RELATIVE_PIXELS); + zephr.set_y_constraint(&color_picker_con, 320 - text_size.y / 2 + 6, .RELATIVE_PIXELS); + zephr.set_width_constraint(&color_picker_con, tile_size, .RELATIVE_PIXELS); + zephr.set_height_constraint(&color_picker_con, tile_size, .RELATIVE_PIXELS); + zephr.draw_color_picker(&color_picker_con, &game.palette.tile_colors[i], .TOP_LEFT, .ACTIVE, cast(u32)i); + + tile_val := cast(u16)math.pow(2, cast(f32)i + 1); + buf: [16]byte + text := fmt.bprintf(buf[:], "%d", tile_val); + font_size := get_tile_font_size(tile_val) / 2; + tile_color := get_tile_color(tile_val); + zephr.set_parent_constraint(&text_con, &color_picker_con); + zephr.set_x_constraint(&text_con, 0, .RELATIVE_PIXELS); + zephr.set_y_constraint(&text_con, 0, .RELATIVE_PIXELS); + zephr.draw_text(text, font_size, text_con, zephr.mult_color(tile_color, 0.5), .CENTER); + } + + btn_con := zephr.DEFAULT_UI_CONSTRAINTS + zephr.set_parent_constraint(&btn_con, &content_card_con); + zephr.set_x_constraint(&btn_con, 0, .RELATIVE_PIXELS); + zephr.set_y_constraint(&btn_con, 400, .RELATIVE_PIXELS); + zephr.set_width_constraint(&btn_con, 150, .RELATIVE_PIXELS); + zephr.set_height_constraint(&btn_con, 0.35, .ASPECT_RATIO); + style = zephr.UiStyle{ + bg_color = zephr.Color{201, 146, 126, 255}, + fg_color = zephr.COLOR_BLACK, + border_radius = 8, + align = .TOP_CENTER, + }; + if (zephr.draw_button(&btn_con, "Reset", style, .ACTIVE)) { + reset_palette(); + } + + zephr.set_width_constraint(&btn_con, 56, .RELATIVE_PIXELS); + zephr.set_height_constraint(&btn_con, 1, .ASPECT_RATIO); + zephr.set_y_constraint(&btn_con, -24, .RELATIVE_PIXELS); + zephr.set_x_constraint(&btn_con, 24, .RELATIVE_PIXELS); + style = zephr.UiStyle{ + bg_color = zephr.mult_color(zephr.COLOR_WHITE, 0.8), + fg_color = zephr.Color{232, 28, 36, 255}, + border_radius = btn_con.height * 0.2, + align = .TOP_RIGHT, + } + if (zephr.draw_icon_button(&btn_con, game.icon_textures[.CLOSE_ICON], style, .ACTIVE)) { + game.settings_dialog = false; + } +} + +draw_help_dialog :: proc() { + dialog_con := zephr.DEFAULT_UI_CONSTRAINTS; + zephr.set_width_constraint(&dialog_con, 1, .RELATIVE); + zephr.set_height_constraint(&dialog_con, 1, .RELATIVE); + zephr.set_x_constraint(&dialog_con, 0, .FIXED); + zephr.set_y_constraint(&dialog_con, 0, .FIXED); + style := zephr.UiStyle { + bg_color = zephr.Color{0, 0, 0, 200}, + align = .CENTER, + }; + zephr.draw_quad(&dialog_con, style); + + content_card_con := zephr.DEFAULT_UI_CONSTRAINTS; + zephr.set_parent_constraint(&content_card_con, &dialog_con); + zephr.set_width_constraint(&content_card_con, 0.4, .RELATIVE); + zephr.set_height_constraint(&content_card_con, 0.33, .RELATIVE); + style = zephr.UiStyle{ + bg_color = zephr.Color{135, 124, 124, 255}, + border_radius = 8, + align = .CENTER, + }; + zephr.draw_quad(&content_card_con, style); + + text_con := zephr.DEFAULT_UI_CONSTRAINTS; + zephr.set_parent_constraint(&text_con, &content_card_con); + zephr.set_width_constraint(&text_con, 1, .RELATIVE_PIXELS); + zephr.set_x_constraint(&text_con, 0, .FIXED); + zephr.set_y_constraint(&text_con, 40, .RELATIVE_PIXELS); + zephr.draw_text("How to play", 64, text_con, zephr.COLOR_YELLOW, .TOP_CENTER); + + zephr.set_y_constraint(&text_con, 30, .RELATIVE_PIXELS); + zephr.set_height_constraint(&text_con, 0.9, .RELATIVE); + zephr.draw_text("Use the arrow keys to move the tiles.\n" + + "When two tiles with the same number touch, they\n" + + "merge into one!\n\nYour goal is to reach 2048 without filling all the tiles", 30., text_con, zephr.COLOR_WHITE, .CENTER); + + btn_con := zephr.DEFAULT_UI_CONSTRAINTS + zephr.set_parent_constraint(&btn_con, &content_card_con); + zephr.set_width_constraint(&btn_con, 56, .RELATIVE_PIXELS); + zephr.set_height_constraint(&btn_con, 1, .ASPECT_RATIO); + zephr.set_y_constraint(&btn_con, -24, .RELATIVE_PIXELS); + zephr.set_x_constraint(&btn_con, 24, .RELATIVE_PIXELS); + style = zephr.UiStyle{ + bg_color = zephr.mult_color(zephr.COLOR_WHITE, 0.8), + fg_color = zephr.Color{232, 28, 36, 255}, + border_radius = btn_con.height * 0.2, + align = .TOP_RIGHT, + }; + if (zephr.draw_icon_button(&btn_con, game.icon_textures[.CLOSE_ICON], style, .ACTIVE)) { + game.help_dialog = false; + } +} + +draw_game_over :: proc() { + dialog_con := zephr.DEFAULT_UI_CONSTRAINTS; + zephr.set_width_constraint(&dialog_con, 1, .RELATIVE); + zephr.set_height_constraint(&dialog_con, 1, .RELATIVE); + zephr.set_x_constraint(&dialog_con, 0, .FIXED); + zephr.set_y_constraint(&dialog_con, 0, .FIXED); + style := zephr.UiStyle { + bg_color = zephr.Color{0, 0, 0, cast(u8)game.game_over_bg_opacity}, + align = .CENTER, + }; + zephr.draw_quad(&dialog_con, style); + + content_card_con := zephr.DEFAULT_UI_CONSTRAINTS; + zephr.set_parent_constraint(&content_card_con, &dialog_con); + zephr.set_width_constraint(&content_card_con, 0.3, .RELATIVE); + zephr.set_height_constraint(&content_card_con, 0.2, .RELATIVE); + style = zephr.UiStyle{ + bg_color = zephr.Color{135, 124, 124, cast(u8)game.game_over_opacity}, + border_radius = 8, + align = .CENTER, + }; + zephr.draw_quad(&content_card_con, style); + + text_con := zephr.DEFAULT_UI_CONSTRAINTS; + zephr.set_parent_constraint(&text_con, &content_card_con); + zephr.set_width_constraint(&text_con, 1, .RELATIVE_PIXELS); + zephr.set_x_constraint(&text_con, 0, .FIXED); + zephr.set_y_constraint(&text_con, 40, .RELATIVE_PIXELS); + zephr.draw_text("Game Over!", 64, text_con, zephr.Color{255, 255, 0, cast(u8)game.game_over_opacity}, .TOP_CENTER); + + btn_con := zephr.DEFAULT_UI_CONSTRAINTS + btn_state: zephr.ButtonState = game.quit_dialog ? .INACTIVE : game.game_over_opacity > 0 ? .ACTIVE : .INACTIVE; + zephr.set_parent_constraint(&btn_con, &content_card_con); + zephr.set_x_constraint(&btn_con, 0, .RELATIVE_PIXELS); + zephr.set_y_constraint(&btn_con, -24, .RELATIVE_PIXELS); + zephr.set_width_constraint(&btn_con, 0.30, .RELATIVE); + zephr.set_height_constraint(&btn_con, 0.3, .ASPECT_RATIO); + style = zephr.UiStyle{ + bg_color = zephr.Color{201, 146, 126, cast(u8)game.game_over_opacity}, + fg_color = zephr.Color{0, 0, 0, cast(u8)game.game_over_opacity}, + border_radius = 8, + align = .BOTTOM_CENTER, + }; + if (zephr.draw_button(&btn_con, "Try Again", style, btn_state)) { + reset_game(); + } +} + +draw_quit_dialog :: proc() { + dialog_con := zephr.DEFAULT_UI_CONSTRAINTS; + zephr.set_x_constraint(&dialog_con, 0, .FIXED); + zephr.set_y_constraint(&dialog_con, 0, .FIXED); + zephr.set_width_constraint(&dialog_con, 1, .RELATIVE); + zephr.set_height_constraint(&dialog_con, 1, .RELATIVE); + style := zephr.UiStyle { + bg_color = zephr.Color{0, 0, 0, 200}, + align = .CENTER, + }; + zephr.draw_quad(&dialog_con, style); + + zephr.set_width_constraint(&dialog_con, 0.4, .RELATIVE); + zephr.set_height_constraint(&dialog_con, 0.2, .RELATIVE); + style = zephr.UiStyle{ + bg_color = zephr.Color{135, 124, 124, 255}, + border_radius = 8, + align = .CENTER, + }; + zephr.draw_quad(&dialog_con, style); + + text_con := zephr.DEFAULT_UI_CONSTRAINTS + zephr.set_parent_constraint(&text_con, &dialog_con); + zephr.set_x_constraint(&text_con, 0, .FIXED); + zephr.set_y_constraint(&text_con, -0.25, .RELATIVE); + zephr.set_width_constraint(&text_con, 1, .RELATIVE_PIXELS); + zephr.draw_text("Are you sure you want to quit?", 36, text_con, zephr.COLOR_WHITE, .CENTER); + + btn_con := zephr.DEFAULT_UI_CONSTRAINTS + zephr.set_parent_constraint(&btn_con, &dialog_con); + zephr.set_width_constraint(&btn_con, 0.25, .RELATIVE); + zephr.set_height_constraint(&btn_con, 0.25, .RELATIVE); + zephr.set_y_constraint(&btn_con, -0.10, .RELATIVE); + zephr.set_x_constraint(&btn_con, -0.20, .RELATIVE); + style = zephr.UiStyle{ + bg_color = zephr.mult_color(zephr.COLOR_WHITE, 0.9), + fg_color = zephr.COLOR_BLACK, + border_radius = btn_con.height * 0.2, + align = .BOTTOM_CENTER, + }; + if (zephr.draw_button(&btn_con, "Yes", style, .ACTIVE)) { + zephr.quit(); + } + + zephr.set_y_constraint(&btn_con, -0.10, .RELATIVE); + zephr.set_x_constraint(&btn_con, 0.20, .RELATIVE); + if (zephr.draw_button(&btn_con, "No", style, .ACTIVE)) { + game.quit_dialog = false; + } +} diff --git a/glx/constants.odin b/glx/constants.odin new file mode 100644 index 0000000..9ff0a89 --- /dev/null +++ b/glx/constants.odin @@ -0,0 +1,11 @@ +package glx + +RENDER_TYPE :: 0x8011 +RGBA_BIT :: 0x00000001 +DEPTH_SIZE :: 12 +DOUBLEBUFFER :: 5 +SAMPLES :: 100001 +CONTEXT_MAJOR_VERSION_ARB :: 0x2091 +CONTEXT_MINOR_VERSION_ARB :: 0x2092 +CONTEXT_PROFILE_MASK_ARB :: 0x9126 +CONTEXT_CORE_PROFILE_BIT_ARB :: 0x00000001 diff --git a/glx/wrappers.odin b/glx/wrappers.odin new file mode 100644 index 0000000..6e8724f --- /dev/null +++ b/glx/wrappers.odin @@ -0,0 +1,97 @@ +package glx + +import x11 "vendor:x11/xlib" +foreign import glx "system:GL" + +Drawable :: x11.XID + +@private +__FBConfigRec :: struct { + visualType: i32, + + transparentType: i32, + transparentRed, transparentGreen, transparentBlue, transparentAlpha: i32, + transparentIndex: i32, + + visualCaveat: i32, + + associatedVisualId: i32, + screen: i32, + + drawableType: i32, + renderType: i32, + + maxPbufferWidth, maxPbufferHeight, maxPbufferPixels: i32, + optimalPbufferWidth, optimalPbufferHeight: i32, /* for SGIX_pbuffer */ + + visualSelectGroup: i32, /* visuals grouped by select priority */ + + id: u32, + + rgbMode: b8, + colorIndexMode: b8, + doubleBufferMode: b8, + stereoMode: b8, + haveAccumBuffer: b8, + haveDepthBuffer: b8, + haveStencilBuffer: b8, + + /* The number of bits present in various buffers */ + accumRedBits, accumGreenBits, accumBlueBits, accumAlphaBits: i32, + depthBits: i32, + stencilBits: i32, + indexBits: i32, + redBits, greenBits, blueBits, alphaBits: i32, + redMask, greenMask, blueMask, alphaMask: u32, + + multiSampleSize: u32,/* Number of samples per pixel (0 if no ms) */ + + nMultiSampleBuffers: u32,/* Number of available ms buffers */ + maxAuxBuffers: i32, + + /* frame buffer level */ + level: i32, + + /* color ranges (for SGI_color_range) */ + extendedRange: b8, + minRed, maxRed: f64, + minGreen, maxGreen: f64, + minBlue, maxBlue: f64, + minAlpha, maxAlpha: f64, +} + +// apparently this isn't defined anywhere ??? +// so we define a minimal one according to this +// https://dri.freedesktop.org/docs/gallium/glxapi_8h-source.html +// see also: https://github.com/Tarnyko/EGLX/blob/b82af86a8a0daf3408fb7a6e7b07b02b01476626/EGLX.h#L280 +@private +__ContextRec :: struct { + currentDpy: ^x11.Display, + isDirect: b8, + currentDrawable: Drawable, + currentReadable: Drawable, + xid: x11.XID +} + +FBConfig :: ^__FBConfigRec +Context :: ^__ContextRec + +foreign glx { + @(link_name = "glXGetProcAddressARB") + GetProcAddressARB :: proc(name: ^u8) -> rawptr --- + @(link_name = "glXSwapBuffers") + SwapBuffers :: proc(display: ^x11.Display, drawable: Drawable) --- + @(link_name = "glXQueryVersion") + QueryVersion :: proc(display: ^x11.Display, major: ^i32, minor: ^i32) -> bool --- + @(link_name = "glXChooseFBConfig") + ChooseFBConfig :: proc(display: ^x11.Display, screen: i32, attrib_list: ^i32, nelements: ^i32) -> [^]FBConfig --- + @(link_name = "glXCreateContextAttribsARB") + CreateContextAttribsARB :: proc(display: ^x11.Display, config: FBConfig, share_context: Context, direct: bool, attrib_list: ^i32) -> Context --- + @(link_name = "glXDestroyContext") + DestroyContext :: proc(display: ^x11.Display, ctx: Context) --- + @(link_name = "glXSwapIntervalEXT") + SwapIntervalEXT :: proc(display: ^x11.Display, drawable: Drawable, interval: i32) -> bool --- + @(link_name = "glXMakeCurrent") + MakeCurrent :: proc(display: ^x11.Display, drawable: Drawable, ctx: Context) -> bool --- +} + diff --git a/main.odin b/main.odin new file mode 100644 index 0000000..4b3e12c --- /dev/null +++ b/main.odin @@ -0,0 +1,18 @@ +package main + +import gl "vendor:OpenGL" +import "glx" +import "zephr" + +font_path :: "assets/fonts/Rubik/Rubik-VariableFont_wght.ttf" +icon_path :: "assets/icon-128x128.png" +title :: "2048" + +main :: proc() { + zephr.init(font_path, icon_path, title, zephr.Vec2{1200, 800}, true) + + game_loop() + + zephr.deinit() +} + diff --git a/shaders/color_chooser.frag b/shaders/color_chooser.frag new file mode 100644 index 0000000..0d41bb9 --- /dev/null +++ b/shaders/color_chooser.frag @@ -0,0 +1,32 @@ +#version 330 core + +in vec2 v_TexCoords; +out vec4 FragColor; + +uniform bool isSlider; +uniform float sliderPercentage; + +//vec3 rgb2hsv(vec3 c){ +// vec4 K = vec4(0.0,-1.0 / 3.0,2.0 / 3.0,-1.0); +// vec4 p = mix(vec4(c.bg,K.wz),vec4(c.gb,K.xy),step(c.b,c.g)); +// vec4 q = mix(vec4(p.xyw,c.r),vec4(c.r,p.yzx),step(p.x,c.r)); +// float d = q.x - min(q.w,q.y); +// float e = 1.0e-10; +// return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)),d / (q.x + e),q.x); +//} + +vec3 hsv2rgb(vec3 color){ + vec3 rgb = clamp(abs(mod(color.r * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0); + rgb = rgb * rgb * (3.0 - 2.0 * rgb); + return color.b * mix(vec3(1.0), rgb, color.g); +} + +void main() { + if (isSlider) { + vec3 color = hsv2rgb(vec3(v_TexCoords.y, 1.0, 1.0)); + FragColor = vec4(color, 1.0); + } else { + vec3 color = hsv2rgb(vec3(sliderPercentage, v_TexCoords.x, 1.0 - v_TexCoords.y)); + FragColor = vec4(color, 1.0); + } +} diff --git a/shaders/font.frag b/shaders/font.frag new file mode 100644 index 0000000..9140f1a --- /dev/null +++ b/shaders/font.frag @@ -0,0 +1,21 @@ +#version 330 core +in vec2 v_TexCoords; +in vec4 textColor; +out vec4 FragColor; + +uniform sampler2D text; + +const float smoothness = 0.05; +const float outlineDistance = 0.50; +const vec4 outlineColor = vec4(0.0, 0.0, 0.0, 1.0); + +void main() { + //float distance = texture(text, v_TexCoords).r; + //float outlineFactor = smoothstep(0.5 - smoothness, 0.5 + smoothness, distance); + //vec4 color = mix(outlineColor, textColor, outlineFactor); + //float alpha = smoothstep(outlineDistance - smoothness, outlineDistance + smoothness, distance); + + vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, v_TexCoords).r); + FragColor = textColor * sampled; + //FragColor = vec4(color.rgb, textColor.a * alpha); +} diff --git a/shaders/font.vert b/shaders/font.vert new file mode 100644 index 0000000..004d09b --- /dev/null +++ b/shaders/font.vert @@ -0,0 +1,21 @@ +#version 330 core +layout (location = 0) in vec2 vertex; + +// per instance +layout (location = 1) in vec4 offset; +layout (location = 2) in int tex_index; +layout (location = 3) in vec4 color; +layout (location = 4) in mat4 model; // this takes locations 4,5,6,7 + +out vec2 v_TexCoords; +out vec4 textColor; +uniform mat4 projection; +// buffer for all the 96 ascii characters' texcoords +uniform vec2 texcoords[96 * 4]; + +void main() { + vec2 pos = vec2((vertex.x) * offset.z, (vertex.y) * offset.w); + gl_Position = projection * model * vec4(pos + offset.xy, 0.0, 1.0); + v_TexCoords = texcoords[tex_index * 4 + gl_VertexID]; + textColor = color; +} diff --git a/shaders/ui.frag b/shaders/ui.frag new file mode 100644 index 0000000..7ffd499 --- /dev/null +++ b/shaders/ui.frag @@ -0,0 +1,40 @@ +#version 330 core + +in vec2 v_TexCoords; +out vec4 FragColor; + +uniform vec4 aColor; +uniform float borderRadius; +uniform float uiWidth; +uniform float uiHeight; +uniform bool hasTexture; + +uniform sampler2D tex; + +const float smoothness = 1.0; + +void main() { + float alpha = aColor.a; + + if (borderRadius > 0.0) { + vec2 dimensions = v_TexCoords * vec2(uiWidth, uiHeight); + float xMax = uiWidth - borderRadius; + float yMax = uiHeight - borderRadius; + + if (dimensions.x < borderRadius && dimensions.y < borderRadius) { + alpha *= 1.0 - smoothstep(borderRadius - smoothness, borderRadius + smoothness, length(dimensions - vec2(borderRadius))); + } else if (dimensions.x < borderRadius && dimensions.y > yMax) { + alpha *= 1.0 - smoothstep(borderRadius - smoothness, borderRadius + smoothness, length(dimensions - vec2(borderRadius, yMax))); + } else if (dimensions.x > xMax && dimensions.y < borderRadius) { + alpha *= 1.0 - smoothstep(borderRadius - smoothness, borderRadius + smoothness, length(dimensions - vec2(xMax, borderRadius))); + } else if (dimensions.x > xMax && dimensions.y > yMax) { + alpha *= 1.0 - smoothstep(borderRadius - smoothness, borderRadius + smoothness, length(dimensions - vec2(xMax, yMax))); + } + } + + if (hasTexture) { + FragColor = texture(tex, v_TexCoords) * vec4(aColor.rgb, alpha); + } else { + FragColor = vec4(aColor.rgb, alpha); + } +} diff --git a/shaders/ui.vert b/shaders/ui.vert new file mode 100644 index 0000000..213850e --- /dev/null +++ b/shaders/ui.vert @@ -0,0 +1,11 @@ +#version 330 core +layout (location = 0) in vec4 vertex; + +out vec2 v_TexCoords; +uniform mat4 projection; +uniform mat4 model; + +void main() { + gl_Position = projection * model * vec4(vertex.xy, 0.0, 1.0); + v_TexCoords = vertex.zw; +} diff --git a/xcursor/wrapper.odin b/xcursor/wrapper.odin new file mode 100644 index 0000000..82daf85 --- /dev/null +++ b/xcursor/wrapper.odin @@ -0,0 +1,9 @@ +package xcursor + +import x11 "vendor:x11/xlib" +foreign import xcursor "system:Xcursor" + +foreign xcursor { + @(link_name = "XcursorLibraryLoadCursor") + LibraryLoadCursor :: proc(display: ^x11.Display, name: cstring) -> x11.Cursor --- +} diff --git a/zephr/audio.odin b/zephr/audio.odin new file mode 100644 index 0000000..c2e8c1c --- /dev/null +++ b/zephr/audio.odin @@ -0,0 +1,3 @@ +package zephr + +// TODO: do audio which requries writing a binding for fmod diff --git a/zephr/shader.odin b/zephr/shader.odin new file mode 100644 index 0000000..966cbb3 --- /dev/null +++ b/zephr/shader.odin @@ -0,0 +1,57 @@ +package zephr + +import "core:fmt" + +import gl "vendor:OpenGL" + +Shader :: struct { + program: u32 +} + +create_shader :: proc(vertex_path: string, fragment_path: string) -> (Shader, bool) { + shader: Shader + + program, success := gl.load_shaders(vertex_path, fragment_path) + shader.program = program; + + return shader, success; +} + +use_shader :: proc(shader: Shader) { + gl.UseProgram(shader.program); +} + +set_mat4f :: proc(shader: Shader, name: cstring, mat4: Mat4, transpose: bool = false) { + mat4 := mat4 + loc := gl.GetUniformLocation(shader.program, name); + gl.UniformMatrix4fv(loc, 1, transpose, raw_data(&mat4)) +} + +set_float :: proc(shader: Shader, name: cstring, val: f32) { + loc := gl.GetUniformLocation(shader.program, name); + gl.Uniform1f(loc, val); +} + +set_int :: proc(shader: Shader, name: cstring, val: i32) { + loc := gl.GetUniformLocation(shader.program, name); + gl.Uniform1i(loc, val); +} + +set_bool :: proc(shader: Shader, name: cstring, val: bool) { + set_int(shader, name, cast(i32)val); +} + +set_vec2f :: proc(shader: Shader, name: cstring, val1: f32, val2: f32) { + loc := gl.GetUniformLocation(shader.program, name); + gl.Uniform2f(loc, val1, val2); +} + +set_vec3f :: proc(shader: Shader, name: cstring, val1: f32, val2: f32, val3: f32) { + loc := gl.GetUniformLocation(shader.program, name); + gl.Uniform3f(loc, val1, val2, val3); +} + +set_vec4f :: proc(shader: Shader, name: cstring, val1: f32, val2: f32, val3: f32, val4: f32) { + loc := gl.GetUniformLocation(shader.program, name); + gl.Uniform4f(loc, val1, val2, val3, val4); +} diff --git a/zephr/text.odin b/zephr/text.odin new file mode 100644 index 0000000..98d1621 --- /dev/null +++ b/zephr/text.odin @@ -0,0 +1,419 @@ +package zephr + +import "core:fmt" + +import gl "vendor:OpenGL" + +import FT "../freetype" + +Character :: struct { + size: Vec2, + bearing: Vec2, + advance: u32, + tex_coords: [4]Vec2, +} + +Font :: struct { + atlas_tex_id: TextureId, + characters: [128]Character, +} + +// GlyphInstance needs to be 100 bytes (meaning no padding) otherwise the data we send to the shader +// is misaligned and we get rendering errors +#assert(size_of(GlyphInstance) == 100) + +GlyphInstance :: struct #packed { + pos: Vec4, + tex_coords_idx: u32, + color: Vec4, + model: Mat4, +} + +GlyphInstanceList :: [dynamic]GlyphInstance + +@private +FONT_PIXEL_SIZE :: 100 +@private +LINE_HEIGHT :: 2.0 + +@private +font_shader : Shader +@private +font_vao : u32 +@private +font_instance_vbo : u32 + + +init_freetype :: proc(font_path: cstring) -> i32 { + ft: FT.Library; + if (FT.Init_FreeType(&ft) != 0) { + return -1; + } + + face: FT.Face; + if (FT.New_Face(ft, font_path, 0, &face) != 0) { + return -2; + } + + // sets the variable font to be bold + /* if ((face->face_flags & FT_FACE_FLAG_MULTIPLE_MASTERS)) { */ + /* printf("[INFO] Got a variable font\n"); */ + /* FT_MM_Var *mm; */ + /* FT_Get_MM_Var(face, &mm); */ + + /* FT_Set_Var_Design_Coordinates(face, mm->num_namedstyles, mm->namedstyle[mm->num_namedstyles - 4].coords); */ + + /* FT_Done_MM_Var(ft, mm); */ + /* } */ + + FT.Set_Pixel_Sizes(face, 0, FONT_PIXEL_SIZE); + + gl.PixelStorei(gl.UNPACK_ALIGNMENT, 1); + + pen_x, pen_y: u32 + + /* FT_UInt glyph_idx; */ + /* FT_ULong c = FT_Get_First_Char(face, &glyph_idx); */ + + tex_width, tex_height: u32; + for i in 32..<128 { + if (FT.Load_Char(face, cast(u64)i, .RENDER) != 0) { + fmt.printf("[ERROR]: failed to load glyph for char '0x%x'\n", i); + } + + /* FT_Render_Glyph(face->glyph, FT_RENDER_MODE_SDF); */ + + tex_width += face.glyph.bitmap.width + 1; + tex_height = max(tex_height, face.glyph.bitmap.rows); + } + + pixels := make([dynamic]u8, tex_width * tex_height); + defer delete(pixels) + + for i in 32..<128 { + /* while (glyph_idx) { */ + if (FT.Load_Char(face, cast(u64)i, .RENDER) != 0) { + fmt.printf("[ERROR]: failed to load glyph for char '0x%x'\n", i); + } + + /* FT_Render_Glyph(face->glyph, FT_RENDER_MODE_SDF); */ + + bmp := &face.glyph.bitmap; + + if (pen_x + bmp.width >= tex_width) { + pen_x = 0; + pen_y += cast(u32)(1 + (face.size.metrics.height >> 6)); + } + + for row in 0.. i32 { + font_vbo, font_ebo: u32; + + res := init_freetype(font_path); + if (res != 0) { + return res; + } + + l_font_shader, success := create_shader("shaders/font.vert", "shaders/font.frag"); + + if (!success) { + fmt.println("[FATAL] Failed to create font shader") + return -2; + } + + font_shader = l_font_shader; + + gl.GenVertexArrays(1, &font_vao); + gl.GenBuffers(1, &font_vbo); + gl.GenBuffers(1, &font_instance_vbo); + gl.GenBuffers(1, &font_ebo); + + quad_vertices := [4][2]f32 { + {0.0, 1.0}, // top left + {1.0, 1.0}, // top right + {1.0, 0.0}, // bottom right + {0.0, 0.0}, // bottom left + }; + + quad_indices := [6]u32 { + 0, 1, 2, + 2, 3, 0, + }; + + gl.BindVertexArray(font_vao); + + // font quad vbo + gl.BindBuffer(gl.ARRAY_BUFFER, font_vbo); + gl.BufferData(gl.ARRAY_BUFFER, size_of(quad_vertices), &quad_vertices, gl.STATIC_DRAW); + gl.EnableVertexAttribArray(0); + gl.VertexAttribPointer(0, 2, gl.FLOAT, gl.FALSE, 2 * size_of(f32), 0); + + // font instance vbo + gl.BindBuffer(gl.ARRAY_BUFFER, font_instance_vbo); + gl.EnableVertexAttribArray(1); + gl.VertexAttribPointer(1, 4, gl.FLOAT, gl.FALSE, size_of(GlyphInstance), 0); + gl.VertexAttribDivisor(1, 1); + gl.EnableVertexAttribArray(2); + gl.VertexAttribIPointer(2, 1, gl.INT, size_of(GlyphInstance), size_of(Vec4)); + gl.VertexAttribDivisor(2, 1); + gl.EnableVertexAttribArray(3); + gl.VertexAttribPointer(3, 4, gl.FLOAT, gl.FALSE, size_of(GlyphInstance), (size_of(Vec4) + size_of(i32))); + gl.VertexAttribDivisor(3, 1); + gl.EnableVertexAttribArray(4); + gl.EnableVertexAttribArray(5); + gl.EnableVertexAttribArray(6); + gl.EnableVertexAttribArray(7); + gl.VertexAttribPointer(4, 4, gl.FLOAT, gl.FALSE, size_of(GlyphInstance), (size_of(Vec4) * 2 + size_of(i32))); + gl.VertexAttribPointer(5, 4, gl.FLOAT, gl.FALSE, size_of(GlyphInstance), (size_of(Vec4) * 3 + size_of(i32))); + gl.VertexAttribPointer(6, 4, gl.FLOAT, gl.FALSE, size_of(GlyphInstance), (size_of(Vec4) * 4 + size_of(i32))); + gl.VertexAttribPointer(7, 4, gl.FLOAT, gl.FALSE, size_of(GlyphInstance), (size_of(Vec4) * 5 + size_of(i32))); + gl.VertexAttribDivisor(4, 1); + gl.VertexAttribDivisor(5, 1); + gl.VertexAttribDivisor(6, 1); + gl.VertexAttribDivisor(7, 1); + + // font ebo + gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, font_ebo); + gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, size_of(quad_indices), &quad_indices, gl.STATIC_DRAW); + + use_shader(font_shader); + + for i in 32..<128 { + for j in 0..<4 { + buf: [24]byte + text := fmt.bprintf(buf[:], "texcoords[%d]", (i - 32) * 4 + j); + set_vec2f(font_shader, cstring(raw_data(text)), zephr_ctx.font.characters[i].tex_coords[j].x, zephr_ctx.font.characters[i].tex_coords[j].y); + } + } + + gl.BindBuffer(gl.ARRAY_BUFFER, 0); + gl.BindVertexArray(0); + + return 0; +} + + +calculate_text_size :: proc(text: string, font_size: u32) -> Vec2 { + scale := cast(f32)font_size / FONT_PIXEL_SIZE; + size : Vec2 = --- + w: f32 = 0; + h: f32 = 0; + max_bearing_h: f32 = 0; + + // NOTE: I don't like looping through the characters twice, but it's fine for now + for i in 0.. 1) { + w -= ch.bearing.x; + } + + // remove the trailing width of the last character + if (i == len(text) - 1) { + w -= ((cast(f32)ch.advance / 64) - (ch.bearing.x + ch.size.x)); + } + + // if we only have one character in the text, then remove the bearing width + if (len(text) == 1) { + w -= (ch.bearing.x); + } + + h = max(h, max_bearing_h - ch.bearing.y + ch.size.y); + + if (text[i] == '\n') { + w = 0; + h += max_bearing_h; + } + } + size.x = w * scale; + size.y = h * scale; + + return size; +} + +draw_text :: proc(text: string, font_size: u32, constraints: UiConstraints, color: Color, alignment: Alignment) { + glyph_instance_list := get_glyph_instance_list_from_text(text, font_size, constraints, color, alignment); + + gl.ActiveTexture(gl.TEXTURE0); + gl.BindTexture(gl.TEXTURE_2D, zephr_ctx.font.atlas_tex_id); + gl.BindVertexArray(font_vao); + + gl.BindBuffer(gl.ARRAY_BUFFER, font_instance_vbo); + gl.BufferData(gl.ARRAY_BUFFER, size_of(GlyphInstance) * len(glyph_instance_list), raw_data(glyph_instance_list), gl.DYNAMIC_DRAW); + + gl.DrawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_INT, nil, cast(i32)len(glyph_instance_list)) + + gl.BindVertexArray(0); + gl.BindTexture(gl.TEXTURE_2D, 0); + + delete(glyph_instance_list) +} + +draw_text_batch :: proc(batch: ^GlyphInstanceList) { + gl.ActiveTexture(gl.TEXTURE0); + gl.BindTexture(gl.TEXTURE_2D, zephr_ctx.font.atlas_tex_id); + gl.BindVertexArray(font_vao); + + gl.BindBuffer(gl.ARRAY_BUFFER, font_instance_vbo); + gl.BufferData(gl.ARRAY_BUFFER, size_of(GlyphInstance) * len(batch), raw_data(batch^), gl.DYNAMIC_DRAW); + + gl.DrawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_INT, nil, cast(i32)len(batch)) + + gl.BindVertexArray(0); + gl.BindTexture(gl.TEXTURE_2D, 0); + + delete(batch^) +} + +get_glyph_instance_list_from_text :: proc(text: string, font_size: u32, constraints: UiConstraints, color: Color, alignment: Alignment) -> GlyphInstanceList { + constraints := constraints + use_shader(font_shader); + text_color := Vec4{ cast(f32)color.r / 255, cast(f32)color.g / 255, cast(f32)color.b / 255, cast(f32)color.a / 255 }; + + set_mat4f(font_shader, "projection", zephr_ctx.projection) + + rect: Rect + + apply_constraints(&constraints, &rect.pos, &rect.size); + + text_size := calculate_text_size(text, FONT_PIXEL_SIZE); + font_scale := cast(f32)font_size / FONT_PIXEL_SIZE * rect.size.x; + + apply_alignment(alignment, &constraints, Vec2{ text_size.x * font_scale, text_size.y * font_scale }, &rect.pos); + + model := identity() + scale(&model, Vec3{font_scale, font_scale, 1}); + + // rotate around the center point of the text + translate(&model, Vec3{-text_size.x * font_scale / 2, -text_size.y * font_scale / 2, 0}); + rotate(&model, constraints.rotation, Vec3{0, 0, 1}); + translate(&model, Vec3{text_size.x *font_scale / 2, text_size.y * font_scale / 2, 0}); + + translate(&model, Vec3{rect.pos.x, rect.pos.y, 0}); + + max_bearing_h: f32 = 0; + for i in 0..> 6); + c += 1; + } + + return glyph_instance_list; +} + +add_text_instance :: proc(batch: ^GlyphInstanceList, text: string, font_size: u32, constraints: UiConstraints, color: Color, alignment: Alignment) { + glyph_instance_list := get_glyph_instance_list_from_text(text, font_size, constraints, color, alignment); + + append(batch, ..glyph_instance_list[:]) + + delete(glyph_instance_list); +} + diff --git a/zephr/texture.odin b/zephr/texture.odin new file mode 100644 index 0000000..1efa36f --- /dev/null +++ b/zephr/texture.odin @@ -0,0 +1,45 @@ +package zephr + +import gl "vendor:OpenGL" +import stb "vendor:stb/image" + +TextureId :: u32 + +load_texture :: proc(path: cstring) -> TextureId { + texture_id: TextureId; + width, height, channels: i32 + data := stb.load(path, &width, &height, &channels, 4) // RGBA + defer stb.image_free(data) + + format := gl.RGBA; + + switch (channels) { + case 1: + format = gl.RED; + break; + case 2: + format = gl.RG; + break; + case 3: + format = gl.RGB; + break; + case 4: + format = gl.RGBA; + break; + } + + gl.GenTextures(1, &texture_id); + gl.BindTexture(gl.TEXTURE_2D, texture_id); + + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); + + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + + gl.TexImage2D(gl.TEXTURE_2D, 0, cast(i32)format, width, height, 0, cast(u32)format, gl.UNSIGNED_BYTE, data); + + gl.GenerateMipmap(gl.TEXTURE_2D); + + return texture_id; +} diff --git a/zephr/timer.odin b/zephr/timer.odin new file mode 100644 index 0000000..b22fe1b --- /dev/null +++ b/zephr/timer.odin @@ -0,0 +1,35 @@ +package zephr + +import "core:fmt" +import "core:time" + +TimerState :: enum { + RUNNING, + PAUSED, + STOPPED, +} + +Timer :: struct { + start: f64, + elapsed: f64, + duration: f32, + state: TimerState, +} + +@private +TIMER : time.Stopwatch + +@private +start_internal_timer :: proc() { + time.stopwatch_start(&TIMER) +} + +get_time :: proc() -> time.Duration { + return time.stopwatch_duration(TIMER) +} + +start_timer :: proc(timer: ^time.Stopwatch) { + time.stopwatch_start(timer) +} + +// TODO: incomplete diff --git a/zephr/ui.odin b/zephr/ui.odin new file mode 100644 index 0000000..6181a7f --- /dev/null +++ b/zephr/ui.odin @@ -0,0 +1,938 @@ +package zephr + +import "core:fmt" +import "core:os" +import "core:mem" + +import gl "vendor:OpenGL" + +UiIdHash :: u32 + +Alignment :: enum { + TOP_LEFT, + TOP_CENTER, + TOP_RIGHT, + LEFT_CENTER, + CENTER, + RIGHT_CENTER, + BOTTOM_LEFT, + BOTTOM_CENTER, + BOTTOM_RIGHT, +} + +UiConstraint :: enum { + FIXED, + RELATIVE, + RELATIVE_PIXELS, + ASPECT_RATIO, +} + +ButtonState :: enum { + ACTIVE, + INACTIVE, + DISABLED, +} + +UiConstraints :: struct { + x: f32, + y: f32, + width: f32, + height: f32, + rotation: f32, + scale: Vec2, + parent: ^UiConstraints, +} + +Rect :: struct { + pos: Vec2, + size: Vec2, +} + +UiStyle :: struct { + bg_color: Color, + fg_color: Color, + border_radius: f32, + align: Alignment, +} + +Ui :: struct { + active_element: UiIdHash, + popup_open: bool, + popup_parent_hash: UiIdHash, + popup_rect: Rect, + popup_parent_constraints: UiConstraints, + popup_revert_color: ^Color, +} + +@private +ui_shader: Shader +@private +color_chooser_shader: Shader +@private +ui_vao: u32 +@private +ui_vbo: u32 + +DEFAULT_UI_CONSTRAINTS :: UiConstraints{ + x = 0, + y = 0, + width = 0, + height = 0, + rotation = 0, + scale = Vec2{1, 1}, + parent = nil, +} + +@private +ui_init :: proc(font_path: cstring) { + res := init_fonts(font_path); + if (res == -1) { + fmt.printf("[ERROR]: could not initialize freetype library\n"); + os.exit(1) + } else if (res == -2) { + fmt.printf("[ERROR]: could not load font file: \"%s\"\n", font_path); + os.exit(1) + } else if (res != 0) { + os.exit(1) + } + + l_ui_shader, success1 := create_shader("shaders/ui.vert", "shaders/ui.frag") + l_color_chooser_shader, success2 := create_shader("shaders/ui.vert", "shaders/color_chooser.frag") + + ui_shader = l_ui_shader + color_chooser_shader = l_color_chooser_shader + + if !success1 || !success2 { + fmt.printf("[ERROR]: could not load ui shaders\n"); + os.exit(1) + } + + gl.GenVertexArrays(1, &ui_vao); + gl.GenBuffers(1, &ui_vbo); + + gl.BindVertexArray(ui_vao); + gl.BindBuffer(gl.ARRAY_BUFFER, ui_vbo); + gl.BufferData(gl.ARRAY_BUFFER, size_of(f32) * 6 * 4, nil, gl.DYNAMIC_DRAW); + gl.EnableVertexAttribArray(0); + gl.VertexAttribPointer(0, 4, gl.FLOAT, gl.FALSE, 4 * size_of(f32), 0); + + gl.BindBuffer(gl.ARRAY_BUFFER, 0); + gl.BindVertexArray(0); +} + +set_parent_constraint :: proc(constraints: ^UiConstraints, parent_constraints: ^UiConstraints) { + constraints.parent = parent_constraints; +} + +set_x_constraint :: proc(constraints: ^UiConstraints, value: f32, type: UiConstraint) { + switch (type) { + case .ASPECT_RATIO: // no-op. fall back to FIXED + fallthrough + case .FIXED: + constraints.x = value; + case .RELATIVE: + if (constraints.parent != nil) { + constraints.x = (value * constraints.parent.width); + } else { + constraints.x = (value * zephr_ctx.window.size.x); + } + case .RELATIVE_PIXELS: + constraints.x = zephr_ctx.window.size.x / zephr_ctx.screen_size.x * value; + } + + if (constraints.parent != nil) { + constraints.x += constraints.parent.x; + } +} + +set_y_constraint :: proc(constraints: ^UiConstraints, value: f32, type: UiConstraint) { + switch (type) { + case .ASPECT_RATIO: // no-op. fall back to FIXED + fallthrough + case .FIXED: + constraints.y = value; + case .RELATIVE: + if (constraints.parent != nil) { + constraints.y = (value * constraints.parent.height); + } else { + constraints.y = (value * zephr_ctx.window.size.y); + } + case .RELATIVE_PIXELS: + constraints.y = zephr_ctx.window.size.y / zephr_ctx.screen_size.y * value; + } + + if (constraints.parent != nil) { + constraints.y += constraints.parent.y; + } +} + +set_width_constraint :: proc(constraints: ^UiConstraints, value: f32, type: UiConstraint) { + switch (type) { + case .FIXED: + constraints.width = value; + case .RELATIVE: + if (constraints.parent != nil) { + constraints.width = (value * constraints.parent.width); + } else { + constraints.width = (value * zephr_ctx.window.size.x); + } + case .RELATIVE_PIXELS: + constraints.width = zephr_ctx.window.size.x / zephr_ctx.screen_size.x * value; + case .ASPECT_RATIO: + constraints.width = constraints.height * value; + } +} + +set_height_constraint :: proc(constraints: ^UiConstraints, value: f32, type: UiConstraint) { + switch (type) { + case .FIXED: + constraints.height = value; + case .RELATIVE: + if (constraints.parent != nil) { + constraints.height = (value * constraints.parent.height); + } else { + constraints.height = (value * zephr_ctx.window.size.y); + } + case .RELATIVE_PIXELS: + constraints.height = zephr_ctx.window.size.y / zephr_ctx.screen_size.y * value; + case .ASPECT_RATIO: + constraints.height = constraints.width * value; + } +} + +set_rotation_constraint :: proc(constraints: ^UiConstraints, angle_d: f32) { + constraints.rotation = angle_d; +} + +@private +apply_constraints :: proc(constraints: ^UiConstraints, pos: ^Vec2, size: ^Vec2) { + pos^ = Vec2{constraints.x, constraints.y}; + size^ = Vec2{constraints.width, constraints.height}; +} + +@private +apply_alignment :: proc(align: Alignment, constraints: ^UiConstraints, size: Vec2, pos: ^Vec2) { + // if we don't have a parent then we're a top level element and we should align against the window + parent_size := Vec2{zephr_ctx.window.size.x, zephr_ctx.window.size.y}; + if (constraints.parent != nil) { + parent_size = Vec2{constraints.parent.width, constraints.parent.height}; + } + + switch (align) { + case .TOP_LEFT: + pos.x = pos.x; + pos.y = pos.y; + case .TOP_CENTER: + pos.x += parent_size.x / 2 - size.x / 2; + pos.y = pos.y; + case .TOP_RIGHT: + pos.x += parent_size.x - size.x; + pos.y = pos.y; + case .BOTTOM_LEFT: + pos.x = pos.x; + pos.y += parent_size.y - size.y; + case .BOTTOM_CENTER: + pos.x += parent_size.x / 2 - size.x / 2; + pos.y += parent_size.y - size.y; + case .BOTTOM_RIGHT: + pos.x += parent_size.x - size.x; + pos.y += parent_size.y - size.y; + case .LEFT_CENTER: + pos.x = pos.x; + pos.y += parent_size.y / 2 - size.y / 2; + case .RIGHT_CENTER: + pos.x += parent_size.x - size.x; + pos.y += parent_size.y / 2 - size.y / 2; + case .CENTER: + pos.x += parent_size.x / 2 - size.x / 2; + pos.y += parent_size.y / 2 - size.y / 2; + } +} + +@private +// TODO: make this handle rotated and scaled rects too +inside_rect :: proc(rect: Rect, point: Vec2) -> bool { + return point.x >= rect.pos.x && point.x <= rect.pos.x + rect.size.x && + point.y >= rect.pos.y && point.y <= rect.pos.y + rect.size.y; +} + +draw_quad :: proc(constraints: ^UiConstraints, style: UiStyle) { + use_shader(ui_shader) + + set_vec4f(ui_shader, "aColor", cast(f32)style.bg_color.r / 255, cast(f32)style.bg_color.g / 255, cast(f32)style.bg_color.b / 255, cast(f32)style.bg_color.a / 255) + set_float(ui_shader, "uiWidth", constraints.width) + set_float(ui_shader, "uiHeight", constraints.height) + set_float(ui_shader, "borderRadius", style.border_radius) + set_mat4f(ui_shader, "projection", zephr_ctx.projection) + + rect: Rect = --- + + apply_constraints(constraints, &rect.pos, &rect.size) + apply_alignment(style.align, constraints, rect.size, &rect.pos) + + // set the positions after applying alignment so children can use them + constraints.x = rect.pos.x; + constraints.y = rect.pos.y; + + model := identity(); + translate(&model, Vec3{-rect.size.x / 2, -rect.size.y / 2, 0}); + scale(&model, Vec3{constraints.scale.x, constraints.scale.y, 1}); + translate(&model, Vec3{rect.size.x / 2, rect.size.y / 2, 0}); + + translate(&model, Vec3{-rect.size.x / 2, -rect.size.y / 2, 0}); + rotate(&model, constraints.rotation, Vec3{0, 0, 1}); + translate(&model, Vec3{rect.size.x / 2, rect.size.y / 2, 0}); + + translate(&model, Vec3{rect.pos.x, rect.pos.y, 0}); + + set_mat4f(ui_shader, "model", model); + + gl.BindVertexArray(ui_vao) + + vertices: [6][4]f32 = { + // bottom left tri + {0, 0 + rect.size.y, 0.0, 1.0}, + {0, 0, 0.0, 0.0}, + {0 + rect.size.x, 0, 1.0, 0.0}, + + // top right tri + {0, 0 + rect.size.y, 0.0, 1.0}, + {0 + rect.size.x, 0, 1.0, 0.0}, + {0 + rect.size.x, 0 + rect.size.y, 1.0, 1.0}, + }; + + gl.BindBuffer(gl.ARRAY_BUFFER, ui_vbo); + gl.BufferSubData(gl.ARRAY_BUFFER, 0, size_of(vertices), raw_data(&vertices)); + gl.BindBuffer(gl.ARRAY_BUFFER, 0); + + gl.DrawArrays(gl.TRIANGLES, 0, 6); + + gl.BindVertexArray(0); +} + +draw_circle :: proc(constraints: ^UiConstraints, style: UiStyle) { + radius: f32 = 0; + + if (constraints.width > constraints.height) { + radius = constraints.height / 2; + } else { + radius = constraints.width / 2; + } + + new_style := style; + new_style.border_radius = radius; + + draw_quad(constraints, new_style); +} + +draw_triangle :: proc(constraints: ^UiConstraints, style: UiStyle) { + use_shader(ui_shader); + + set_vec4f(ui_shader, "aColor", cast(f32)style.bg_color.r / 255, cast(f32)style.bg_color.g / 255, cast(f32)style.bg_color.b / 255, cast(f32)style.bg_color.a / 255); + set_float(ui_shader, "borderRadius", 0); + set_mat4f(ui_shader, "projection", zephr_ctx.projection) + + rect: Rect = --- + + apply_constraints(constraints, &rect.pos, &rect.size); + apply_alignment(style.align, constraints, rect.size, &rect.pos); + + // set the positions after applying alignment so children can use them + constraints.x = rect.pos.x; + constraints.y = rect.pos.y; + + model := identity(); + translate(&model, Vec3{-rect.size.x / 2, -rect.size.y / 2, 0}); + scale(&model, Vec3{constraints.scale.x, constraints.scale.y, 1}); + translate(&model, Vec3{rect.size.x / 2, rect.size.y / 2, 0}); + + translate(&model, Vec3{-rect.size.x / 2, -rect.size.y / 2, 0}); + rotate(&model, constraints.rotation, Vec3{0, 0, 1}); + translate(&model, Vec3{rect.size.x / 2, rect.size.y / 2, 0}); + + translate(&model, Vec3{rect.pos.x, rect.pos.y, 0}); + + set_mat4f(ui_shader, "model", model); + + gl.BindVertexArray(ui_vao); + + vertices := [3][4]f32 { + {0 + rect.size.x, 0, 0.0, 0.0}, + {0, 0, 0.0, 0.0}, + {0 + rect.size.x / 2, 0 + rect.size.y, 0.0, 0.0}, + }; + + gl.BindBuffer(gl.ARRAY_BUFFER, ui_vbo); + gl.BufferSubData(gl.ARRAY_BUFFER, 0, size_of(vertices), raw_data(&vertices)) + gl.BindBuffer(gl.ARRAY_BUFFER, 0); + + gl.DrawArrays(gl.TRIANGLES, 0, 3); + + gl.BindVertexArray(0); +} + +draw_texture :: proc(constraints: ^UiConstraints, texture_id: TextureId, color: Color, radius: f32, align: Alignment) { + use_shader(ui_shader); + + set_vec4f(ui_shader, "aColor", cast(f32)color.r / 255, cast(f32)color.g / 255, cast(f32)color.b / 255, cast(f32)color.a / 255); + set_float(ui_shader, "uiWidth", constraints.width); + set_float(ui_shader, "uiHeight", constraints.height); + set_float(ui_shader, "borderRadius", radius); + set_bool(ui_shader, "hasTexture", true); + + set_mat4f(ui_shader, "projection", zephr_ctx.projection) + + rect: Rect = --- + + apply_constraints(constraints, &rect.pos, &rect.size); + apply_alignment(align, constraints, rect.size, &rect.pos); + + // set the positions after applying alignment so children can use them + constraints.x = rect.pos.x; + constraints.y = rect.pos.y; + + model := identity(); + translate(&model, Vec3{-rect.size.x / 2, -rect.size.y / 2, 0}); + scale(&model, Vec3{constraints.scale.x, constraints.scale.y, 1}); + translate(&model, Vec3{rect.size.x / 2, rect.size.y / 2, 0}); + + translate(&model, Vec3{-rect.size.x / 2, -rect.size.y / 2, 0}); + rotate(&model, constraints.rotation, Vec3{0, 0, 1}); + translate(&model, Vec3{rect.size.x / 2, rect.size.y / 2, 0}); + + translate(&model, Vec3{rect.pos.x, rect.pos.y, 0}); + + set_mat4f(ui_shader, "model", model) + + gl.BindVertexArray(ui_vao); + + vertices := [6][4]f32 { + // bottom left tri + {0, 0 + rect.size.y, 0.0, 1.0}, + {0, 0, 0.0, 0.0}, + {0 + rect.size.x, 0, 1.0, 0.0}, + + // top right tri + {0, 0 + rect.size.y, 0.0, 1.0}, + {0 + rect.size.x, 0, 1.0, 0.0}, + {0 + rect.size.x, 0 + rect.size.y, 1.0, 1.0}, + }; + + gl.BindBuffer(gl.ARRAY_BUFFER, ui_vbo); + gl.BufferSubData(gl.ARRAY_BUFFER, 0, size_of(vertices), raw_data(&vertices)) + gl.BindBuffer(gl.ARRAY_BUFFER, 0); + + gl.ActiveTexture(gl.TEXTURE0); + gl.BindTexture(gl.TEXTURE_2D, texture_id); + + gl.DrawArrays(gl.TRIANGLES, 0, 6); + + gl.BindVertexArray(0); + set_bool(ui_shader, "hasTexture", false); +} + +draw_button :: proc(constraints: ^UiConstraints, text: string, style: UiStyle, state: ButtonState, caller := #caller_location) -> bool { + line := caller.line + line_bytes := mem.byte_slice(&line, size_of(line)) + hash := fnv_hash32(transmute([]byte)caller.file_path, cast(u32)len(caller.file_path), FNV_HASH32_INIT) + hash = fnv_hash32(line_bytes, size_of(caller.line), hash) + + style := style + rect: Rect = --- + + apply_constraints(constraints, &rect.pos, &rect.size); + apply_alignment(style.align, constraints, rect.size, &rect.pos); + + is_hovered := inside_rect(rect, zephr_ctx.mouse.pos); + is_held := zephr_ctx.ui.active_element == hash + left_mouse_pressed := zephr_ctx.mouse.pressed && zephr_ctx.mouse.button == .BUTTON_LEFT; + left_mouse_released := zephr_ctx.mouse.released && zephr_ctx.mouse.button == .BUTTON_LEFT; + clicked := false; + + if (zephr_ctx.ui.active_element == 0) { + if (is_hovered && left_mouse_pressed) { + zephr_ctx.ui.active_element = hash; + } + } else if (zephr_ctx.ui.active_element == hash) { + if (left_mouse_released) { + zephr_ctx.ui.active_element = 0; + + if (is_hovered) { + clicked = true; + } + } + } + + if (is_hovered && state == .ACTIVE) { + set_cursor(Cursor.HAND); + + if (is_held) { + style.bg_color = mult_color(style.bg_color, 0.8); + draw_quad(constraints, style); + } else { + style.bg_color = mult_color(style.bg_color, 0.9); + draw_quad(constraints, style); + } + } else if (state == .DISABLED) { + if (is_hovered) { + set_cursor(Cursor.DISABLED); + } + + style.bg_color.a = 100; + style.fg_color.a = 100; + draw_quad(constraints, style); + } else { + draw_quad(constraints, style); + } + + if (text != "") { + text_constraints := DEFAULT_UI_CONSTRAINTS + + set_parent_constraint(&text_constraints, constraints); + set_x_constraint(&text_constraints, 0, .RELATIVE_PIXELS); + set_y_constraint(&text_constraints, 0, .RELATIVE_PIXELS); + set_width_constraint(&text_constraints, 1.0, .RELATIVE_PIXELS); + + font_size := ((constraints.width) / (text_constraints.width * 0.1 * 65)); + + // TODO: need to adjust the font size based on the text height + + draw_text(text, cast(u32)font_size, text_constraints, style.fg_color, .CENTER); + } + + if (clicked && state == .ACTIVE) { + return true; + } + + return false; +} + +draw_icon_button :: proc(constraints: ^UiConstraints, icon_tex_id: TextureId, style: UiStyle, state: ButtonState, caller := #caller_location) -> bool { + assert(icon_tex_id != 0, "draw_icon_button() requires that you provide an icon texture"); + + line := caller.line + line_bytes := mem.byte_slice(&line, size_of(line)) + hash := fnv_hash32(transmute([]byte)caller.file_path, cast(u32)len(caller.file_path), FNV_HASH32_INIT) + hash = fnv_hash32(line_bytes, size_of(caller.line), hash) + + style := style + rect: Rect = --- + + apply_constraints(constraints, &rect.pos, &rect.size); + apply_alignment(style.align, constraints, rect.size, &rect.pos); + + is_held := zephr_ctx.ui.active_element == hash; + is_hovered := inside_rect(rect, zephr_ctx.mouse.pos); + left_mouse_pressed := zephr_ctx.mouse.pressed && zephr_ctx.mouse.button == .BUTTON_LEFT; + left_mouse_released := zephr_ctx.mouse.released && zephr_ctx.mouse.button == .BUTTON_LEFT; + clicked := false; + + if (zephr_ctx.ui.active_element == 0) { + if (is_hovered && left_mouse_pressed) { + zephr_ctx.ui.active_element = hash; + } + } else if (zephr_ctx.ui.active_element == hash) { + if (left_mouse_released) { + zephr_ctx.ui.active_element = 0; + + if (is_hovered) { + clicked = true; + } + } + } + + if (is_hovered && state == .ACTIVE) { + set_cursor(.HAND); + + if (is_held) { + style.bg_color = mult_color(style.bg_color, 0.8); + style.fg_color = mult_color(style.fg_color, 0.8); + } else { + style.bg_color = mult_color(style.bg_color, 0.9); + style.fg_color = mult_color(style.fg_color, 0.9); + } + } else if (state == .DISABLED) { + if (is_hovered) { + set_cursor(.DISABLED); + } + + style.fg_color.a = 100; + style.bg_color.a = 100; + } + + draw_quad(constraints, style); + + icon_constraints := DEFAULT_UI_CONSTRAINTS + + set_parent_constraint(&icon_constraints, constraints); + set_x_constraint(&icon_constraints, 0, .RELATIVE_PIXELS); + set_y_constraint(&icon_constraints, 0, .RELATIVE_PIXELS); + set_width_constraint(&icon_constraints, constraints.width * 0.8, .FIXED); + set_height_constraint(&icon_constraints, constraints.height * 0.8, .FIXED); + draw_texture(&icon_constraints, icon_tex_id, style.fg_color, 0, .CENTER); + + if (clicked && state == .ACTIVE) { + return true; + } + + return false; +} + +@private +draw_color_picker_slider :: proc(constraints: ^UiConstraints, align: Alignment, caller := #caller_location) -> f32 { + @static + slider_selection: f32 = 0 + @static + dragging := false + + line := caller.line + line_bytes := mem.byte_slice(&line, size_of(line)) + hash := fnv_hash32(transmute([]byte)caller.file_path, cast(u32)len(caller.file_path), FNV_HASH32_INIT); + hash = fnv_hash32(line_bytes, size_of(caller.line), hash); + + use_shader(color_chooser_shader); + + set_bool(color_chooser_shader, "isSlider", true); + set_mat4f(color_chooser_shader, "projection", zephr_ctx.projection); + + rect: Rect = --- + + apply_constraints(constraints, &rect.pos, &rect.size); + apply_alignment(align, constraints, rect.size, &rect.pos); + + // set the positions after applying alignment so children can use them + constraints.x = rect.pos.x; + constraints.y = rect.pos.y; + + is_hovered := inside_rect(rect, zephr_ctx.mouse.pos); + left_mouse_pressed := zephr_ctx.mouse.pressed && zephr_ctx.mouse.button == .BUTTON_LEFT; + left_mouse_released := zephr_ctx.mouse.released && zephr_ctx.mouse.button == .BUTTON_LEFT; + + if (zephr_ctx.ui.active_element == 0) { + if (is_hovered && left_mouse_pressed) { + zephr_ctx.ui.active_element = hash; + dragging = true; + } + } else if (zephr_ctx.ui.active_element == hash) { + if (left_mouse_released) { + zephr_ctx.ui.active_element = 0; + dragging = false; + } + } + + if (dragging) { + slider_selection = clamp((zephr_ctx.mouse.pos.y - constraints.y) / constraints.height, 0, 1); + } + + model := identity(); + translate(&model, Vec3{rect.pos.x, rect.pos.y, 0}) + + set_mat4f(color_chooser_shader, "model", model); + + gl.BindVertexArray(ui_vao); + + vertices := [6][4]f32 { + // bottom left tri + {0, 0 + rect.size.y, 0.0, 1.0}, + {0, 0, 0.0, 0.0}, + {0 + rect.size.x, 0, 1.0, 0.0}, + + // top right tri + {0, 0 + rect.size.y, 0.0, 1.0}, + {0 + rect.size.x, 0, 1.0, 0.0}, + {0 + rect.size.x, 0 + rect.size.y, 1.0, 1.0}, + }; + + gl.BindBuffer(gl.ARRAY_BUFFER, ui_vbo); + gl.BufferSubData(gl.ARRAY_BUFFER, 0, size_of(vertices), raw_data(&vertices)) + gl.BindBuffer(gl.ARRAY_BUFFER, 0); + + gl.DrawArrays(gl.TRIANGLES, 0, 6); + + gl.BindVertexArray(0); + set_bool(color_chooser_shader, "isSlider", false); + + // draw the selection + triangle_con := DEFAULT_UI_CONSTRAINTS + set_parent_constraint(&triangle_con, constraints); + set_x_constraint(&triangle_con, -11, .RELATIVE_PIXELS); + set_y_constraint(&triangle_con, slider_selection, .RELATIVE); + set_rotation_constraint(&triangle_con, -90); + set_width_constraint(&triangle_con, 14, .RELATIVE_PIXELS); + set_height_constraint(&triangle_con, 0.5, .ASPECT_RATIO); + triangle_con.y += -(triangle_con.height / 2); + tri_style := UiStyle { + bg_color = COLOR_BLACK, + border_radius = 0, + align = .TOP_LEFT, + }; + draw_triangle(&triangle_con, tri_style); + + set_x_constraint(&triangle_con, 1, .RELATIVE); + triangle_con.x -= triangle_con.height / 2; + set_rotation_constraint(&triangle_con, 90); + draw_triangle(&triangle_con, tri_style); + + return slider_selection; +} + +@private +draw_color_picker_canvas :: proc(constraints: ^UiConstraints, slider_percentage: f32, align: Alignment, caller := #caller_location) -> Color { + @static + dragging := false; + @static + canvas_pos := Vec2{0, 0}; + + line := caller.line + line_bytes := mem.byte_slice(&line, size_of(line)) + hash := fnv_hash32(transmute([]byte)caller.file_path, cast(u32)len(caller.file_path), FNV_HASH32_INIT); + hash = fnv_hash32(line_bytes, size_of(caller.line), hash); + + use_shader(color_chooser_shader); + + set_bool(color_chooser_shader, "isSlider", false); + set_float(color_chooser_shader, "sliderPercentage", slider_percentage); + set_mat4f(color_chooser_shader, "projection", zephr_ctx.projection); + + rect: Rect = --- + + apply_constraints(constraints, &rect.pos, &rect.size); + apply_alignment(align, constraints, rect.size, &rect.pos); + + // set the positions after applying alignment so children can use them + constraints.x = rect.pos.x; + constraints.y = rect.pos.y; + + is_hovered := inside_rect(rect, zephr_ctx.mouse.pos); + left_mouse_pressed := zephr_ctx.mouse.pressed && zephr_ctx.mouse.button == .BUTTON_LEFT; + left_mouse_released := zephr_ctx.mouse.released && zephr_ctx.mouse.button == .BUTTON_LEFT; + + if (zephr_ctx.ui.active_element == 0) { + if (is_hovered && left_mouse_pressed) { + zephr_ctx.ui.active_element = hash; + dragging = true; + } + } else if (zephr_ctx.ui.active_element == hash) { + if (left_mouse_released) { + zephr_ctx.ui.active_element = 0; + dragging = false; + } + } + + if (dragging) { + x := (zephr_ctx.mouse.pos.x - constraints.x) / constraints.width; + y := (zephr_ctx.mouse.pos.y - constraints.y) / constraints.height; + + canvas_pos.x = clamp(x, 0, 1); + canvas_pos.y = clamp(y, 0, 1); + } + + model := identity(); + translate(&model, Vec3{rect.pos.x, rect.pos.y, 0}); + + set_mat4f(color_chooser_shader, "model", model); + + gl.BindVertexArray(ui_vao); + + vertices := [6][4]f32 { + // bottom left tri + {0, 0 + rect.size.y, 0.0, 1.0}, + {0, 0, 0.0, 0.0}, + {0 + rect.size.x, 0, 1.0, 0.0}, + + // top right tri + {0, 0 + rect.size.y, 0.0, 1.0}, + {0 + rect.size.x, 0, 1.0, 0.0}, + {0 + rect.size.x, 0 + rect.size.y, 1.0, 1.0}, + }; + + gl.BindBuffer(gl.ARRAY_BUFFER, ui_vbo); + gl.BufferSubData(gl.ARRAY_BUFFER, 0, size_of(vertices), raw_data(&vertices)) + gl.BindBuffer(gl.ARRAY_BUFFER, 0); + + gl.DrawArrays(gl.TRIANGLES, 0, 6); + + gl.BindVertexArray(0); + + selected_color := hsv2rgb(slider_percentage * 360, canvas_pos.x, 1.0 - canvas_pos.y); + + // draw the selection + triangle_con := DEFAULT_UI_CONSTRAINTS + set_parent_constraint(&triangle_con, constraints); + set_x_constraint(&triangle_con, canvas_pos.x, .RELATIVE); + set_y_constraint(&triangle_con, canvas_pos.y, .RELATIVE); + set_width_constraint(&triangle_con, 14, .RELATIVE_PIXELS); + set_height_constraint(&triangle_con, 0.5, .ASPECT_RATIO); + triangle_con.x -= triangle_con.height; + triangle_con.y -= triangle_con.height + triangle_con.height / 2 + triangle_con.width / 2; + set_rotation_constraint(&triangle_con, 0); + tri_style := UiStyle { + bg_color = determine_color_contrast(selected_color), + border_radius = 0, + align = .TOP_LEFT, + }; + draw_triangle(&triangle_con, tri_style); + + triangle_con.x += triangle_con.height * 2; + triangle_con.y += triangle_con.height * 2; + set_rotation_constraint(&triangle_con, 90); + draw_triangle(&triangle_con, tri_style); + + triangle_con.x -= triangle_con.height * 2; + triangle_con.y += triangle_con.height * 2; + set_rotation_constraint(&triangle_con, 180); + draw_triangle(&triangle_con, tri_style); + + triangle_con.x -= triangle_con.height * 2; + triangle_con.y -= triangle_con.height * 2; + set_rotation_constraint(&triangle_con, 270); + draw_triangle(&triangle_con, tri_style); + + return selected_color; +} + +// TODO: consume mouse events on the popup +@private +draw_color_picker_popup :: proc(picker_button_con: ^UiConstraints) { + popup_con := DEFAULT_UI_CONSTRAINTS + set_parent_constraint(&popup_con, picker_button_con); + set_x_constraint(&popup_con, 0, .FIXED); + set_y_constraint(&popup_con, 1.4, .RELATIVE); + set_width_constraint(&popup_con, 450, .RELATIVE_PIXELS); + set_height_constraint(&popup_con, 480, .RELATIVE_PIXELS); + popup_style := UiStyle { + bg_color = mult_color(COLOR_WHITE, 0.94), + border_radius = popup_con.width * 0.02, + align = .TOP_LEFT, + }; + draw_quad(&popup_con, popup_style); + + color_slider_con := DEFAULT_UI_CONSTRAINTS + set_parent_constraint(&color_slider_con, &popup_con); + set_x_constraint(&color_slider_con, -0.04, .RELATIVE); + set_y_constraint(&color_slider_con, 0.05, .RELATIVE); + set_height_constraint(&color_slider_con, 400 * 0.9, .RELATIVE_PIXELS); + set_width_constraint(&color_slider_con, 0.1, .ASPECT_RATIO); + slider_selection := draw_color_picker_slider(&color_slider_con, .TOP_RIGHT); + + set_x_constraint(&color_slider_con, 0.04, .RELATIVE); + set_y_constraint(&color_slider_con, 0.05, .RELATIVE); + set_width_constraint(&color_slider_con, popup_con.width * 0.8, .FIXED); + selected_color := draw_color_picker_canvas(&color_slider_con, slider_selection, .TOP_LEFT); + + button_con := DEFAULT_UI_CONSTRAINTS + set_parent_constraint(&button_con, &popup_con); + set_x_constraint(&button_con, 0.04, .RELATIVE); + set_y_constraint(&button_con, -0.05, .RELATIVE); + set_width_constraint(&button_con, 0.43, .RELATIVE); + set_height_constraint(&button_con, 0.3, .ASPECT_RATIO); + button_style := UiStyle { + bg_color = zephr_ctx.ui.popup_revert_color^, + fg_color = determine_color_contrast(zephr_ctx.ui.popup_revert_color^), + border_radius = 4, + align = .BOTTOM_LEFT, + } + if (draw_button(&button_con, "Revert", button_style, .ACTIVE)) { + zephr_ctx.ui.popup_open = false; + zephr_ctx.ui.popup_parent_hash = 0; + } + + button_style.align = .BOTTOM_RIGHT; + button_style.fg_color = determine_color_contrast(selected_color); + set_x_constraint(&button_con, -0.04, .RELATIVE); + set_y_constraint(&button_con, -0.05, .RELATIVE); + button_style.bg_color = selected_color; + if (draw_button(&button_con, "Apply", button_style, .ACTIVE)) { + zephr_ctx.ui.popup_revert_color^ = selected_color; + zephr_ctx.ui.popup_open = false; + zephr_ctx.ui.popup_parent_hash = 0; + } + + rect := Rect { + {popup_con.x, popup_con.y}, + {popup_con.width, popup_con.height}, + }; + + zephr_ctx.ui.popup_rect = rect; +} + +draw_color_picker :: proc(constraints: ^UiConstraints, color: ^Color, align: Alignment, state: ButtonState, id: u32 = 0, caller := #caller_location) { + line := caller.line + line_bytes := mem.byte_slice(&line, size_of(line)) + id := id + id_bytes := mem.byte_slice(&id, size_of(id)) + hash := fnv_hash32(id_bytes, size_of(id), FNV_HASH32_INIT); + hash = fnv_hash32(transmute([]byte)caller.file_path, cast(u32)len(caller.file_path), hash); + hash = fnv_hash32(line_bytes, size_of(line), hash); + + rect: Rect = --- + display_color := color^; + + con := constraints^; + + apply_constraints(&con, &rect.pos, &rect.size); + apply_alignment(align, &con, rect.size, &rect.pos); + + is_held := zephr_ctx.ui.active_element == hash + is_hovered := inside_rect(rect, zephr_ctx.mouse.pos); + left_mouse_pressed := zephr_ctx.mouse.pressed && zephr_ctx.mouse.button == .BUTTON_LEFT; + left_mouse_released := zephr_ctx.mouse.released && zephr_ctx.mouse.button == .BUTTON_LEFT; + clicked := false; + + if (zephr_ctx.ui.active_element == 0) { + if (is_hovered && left_mouse_pressed) { + zephr_ctx.ui.active_element = hash; + } + } else if (zephr_ctx.ui.active_element == hash) { + if (left_mouse_released) { + zephr_ctx.ui.active_element = 0; + + if (is_hovered) { + clicked = true; + } + } + } + + if (is_hovered && state == .ACTIVE) { + set_cursor(.HAND); + con.scale.y *= 1.08; + con.scale.x *= 1.08; + + if (is_held) { + display_color = mult_color(display_color, 0.8); + } else { + display_color = mult_color(display_color, 0.9); + } + } else if (state == .DISABLED) { + if (is_hovered) { + set_cursor(.DISABLED); + } + + display_color.a = 100; + } + + style := UiStyle { + bg_color = display_color, + border_radius = con.width * 0.08, + align = align, + }; + draw_quad(&con, style); + + if (clicked && state == .ACTIVE) { + zephr_ctx.ui.popup_open = true; + zephr_ctx.ui.popup_parent_constraints = con; + zephr_ctx.ui.popup_revert_color = color; + zephr_ctx.ui.popup_parent_hash = hash; + } + + if (zephr_ctx.ui.popup_parent_hash == hash) { + zephr_ctx.ui.popup_open = true; + zephr_ctx.ui.popup_parent_constraints = con; + } + + /* if (is_hovered && state == BUTTON_STATE_ACTIVE && zephr_ctx->mouse.released && zephr_ctx->mouse.button == ZEPHR_MOUSE_BUTTON_LEFT) { */ + /* return true; */ + /* } */ + + /* return false; */ +} diff --git a/zephr/zephr.odin b/zephr/zephr.odin new file mode 100644 index 0000000..e4e3dac --- /dev/null +++ b/zephr/zephr.odin @@ -0,0 +1,1250 @@ +package zephr + +import "core:fmt" + +import x11 "vendor:x11/xlib" +import gl "vendor:OpenGL" +import "vendor:stb/image" + +import "../glx" +import "../xcursor" + +Cursor :: enum { + ARROW, + IBEAM, + CROSSHAIR, + HAND, + HRESIZE, + VRESIZE, + DISABLED, +} + +EventType :: enum { + UNKNOWN, + KEY_PRESSED, + KEY_RELEASED, + MOUSE_BUTTON_PRESSED, + MOUSE_BUTTON_RELEASED, + MOUSE_SCROLL, + MOUSE_MOVED, + WINDOW_RESIZED, + WINDOW_CLOSED, +} + +MouseButton :: enum { + BUTTON_LEFT, + BUTTON_RIGHT, + BUTTON_MIDDLE, + BUTTON_BACK, + BUTTON_FORWARD, + BUTTON_3, + BUTTON_4, + BUTTON_5, + BUTTON_6, + BUTTON_7, +} + +MouseScrollDirection :: enum { + UP, + DOWN, +} + +KeyMod :: bit_set[KeyModBits; u16] +KeyModBits :: enum { + NONE = 0, + + LEFT_SHIFT = 1, + RIGHT_SHIFT = 2, + SHIFT = 3, + + LEFT_CTRL = 4, + RIGHT_CTRL = 5, + CTRL = 6, + + LEFT_ALT = 7, + RIGHT_ALT = 8, + ALT = 9, + + LEFT_META = 10, + RIGHT_META = 11, + META = 12, + + CAPS_LOCK = 13, + NUM_LOCK = 14, +} + +Scancode :: enum { + NULL = 0, + + A = 4, + B = 5, + C = 6, + D = 7, + E = 8, + F = 9, + G = 10, + H = 11, + I = 12, + J = 13, + K = 14, + L = 15, + M = 16, + N = 17, + O = 18, + P = 19, + Q = 20, + R = 21, + S = 22, + T = 23, + U = 24, + V = 25, + W = 26, + X = 27, + Y = 28, + Z = 29, + + KEY_1 = 30, + KEY_2 = 31, + KEY_3 = 32, + KEY_4 = 33, + KEY_5 = 34, + KEY_6 = 35, + KEY_7 = 36, + KEY_8 = 37, + KEY_9 = 38, + KEY_0 = 39, + + ENTER = 40, + ESCAPE = 41, + BACKSPACE = 42, + TAB = 43, + SPACE = 44, + + MINUS = 45, + EQUALS = 46, + LEFT_BRACKET = 47, + RIGHT_BRACKET = 48, + BACKSLASH = 49, + NON_US_HASH = 50, + SEMICOLON = 51, + APOSTROPHE = 52, + GRAVE = 53, + COMMA = 54, + PERIOD = 55, + SLASH = 56, + + CAPS_LOCK = 57, + + F1 = 58, + F2 = 59, + F3 = 60, + F4 = 61, + F5 = 62, + F6 = 63, + F7 = 64, + F8 = 65, + F9 = 66, + F10 = 67, + F11 = 68, + F12 = 69, + + PRINT_SCREEN = 70, + SCROLL_LOCK = 71, + PAUSE = 72, + INSERT = 73, + HOME = 74, + PAGE_UP = 75, + DELETE = 76, + END = 77, + PAGE_DOWN = 78, + RIGHT = 79, + LEFT = 80, + DOWN = 81, + UP = 82, + + NUM_LOCK_OR_CLEAR = 83, + KP_DIVIDE = 84, + KP_MULTIPLY = 85, + KP_MINUS = 86, + KP_PLUS = 87, + KP_ENTER = 88, + KP_1 = 89, + KP_2 = 90, + KP_3 = 91, + KP_4 = 92, + KP_5 = 93, + KP_6 = 94, + KP_7 = 95, + KP_8 = 96, + KP_9 = 97, + KP_0 = 98, + KP_PERIOD = 99, + + NON_US_BACKSLASH = 100, + APPLICATION = 101, + POWER = 102, + KP_EQUALS = 103, + F13 = 104, + F14 = 105, + F15 = 106, + F16 = 107, + F17 = 108, + F18 = 109, + F19 = 110, + F20 = 111, + F21 = 112, + F22 = 113, + F23 = 114, + F24 = 115, + EXECUTE = 116, + HELP = 117, + MENU = 118, + SELECT = 119, + STOP = 120, + AGAIN = 121, + UNDO = 122, + CUT = 123, + COPY = 124, + PASTE = 125, + FIND = 126, + MUTE = 127, + VOLUME_UP = 128, + VOLUME_DOWN = 129, + KP_COMMA = 133, + KP_EQUALSAS400 = 134, + + INTERNATIONAL1 = 135, + INTERNATIONAL2 = 136, + INTERNATIONAL3 = 137, + INTERNATIONAL4 = 138, + INTERNATIONAL5 = 139, + INTERNATIONAL6 = 140, + INTERNATIONAL7 = 141, + INTERNATIONAL8 = 142, + INTERNATIONAL9 = 143, + LANG1 = 144, + LANG2 = 145, + LANG3 = 146, + LANG4 = 147, + LANG5 = 148, + LANG6 = 149, + LANG7 = 150, + LANG8 = 151, + LANG9 = 152, + + ALT_ERASE = 153, + SYSREQ = 154, + CANCEL = 155, + CLEAR = 156, + PRIOR = 157, + ENTER_2 = 158, + SEPARATOR = 159, + OUT = 160, + OPER = 161, + CLEARAGAIN = 162, + CRSEL = 163, + EXSEL = 164, + + KP_00 = 176, + KP_000 = 177, + THOUSANDSSEPARATOR = 178, + DECIMALSEPARATOR = 179, + CURRENCYUNIT = 180, + CURRENCYSUBUNIT = 181, + KP_LEFT_PAREN = 182, + KP_RIGHT_PAREN = 183, + KP_LEFT_BRACE = 184, + KP_RIGHT_BRACE = 185, + KP_TAB = 186, + KP_BACKSPACE = 187, + KP_A = 188, + KP_B = 189, + KP_C = 190, + KP_D = 191, + KP_E = 192, + KP_F = 193, + KP_XOR = 194, + KP_POWER = 195, + KP_PERCENT = 196, + KP_LESS = 197, + KP_GREATER = 198, + KP_AMPERSAND = 199, + KP_DBLAMPERSAND = 200, + KP_VERTICALBAR = 201, + KP_DBLVERTICALBAR = 202, + KP_COLON = 203, + KP_HASH = 204, + KP_SPACE = 205, + KP_AT = 206, + KP_EXCLAM = 207, + KP_MEMSTORE = 208, + KP_MEMRECALL = 209, + KP_MEMCLEAR = 210, + KP_MEMADD = 211, + KP_MEMSUBTRACT = 212, + KP_MEMMULTIPLY = 213, + KP_MEMDIVIDE = 214, + KP_PLUS_MINUS = 215, + KP_CLEAR = 216, + KP_CLEARENTRY = 217, + KP_BINARY = 218, + KP_OCTAL = 219, + KP_DECIMAL = 220, + KP_HEXADECIMAL = 221, + + LEFT_CTRL = 224, + LEFT_SHIFT = 225, + LEFT_ALT = 226, + LEFT_META = 227, + RIGHT_CTRL = 228, + RIGHT_SHIFT = 229, + RIGHT_ALT = 230, + RIGHT_META = 231, + + /** Not a key. Marks the number of scancodes. */ + ZEPHR_KEYCODE_COUNT = 512, +} + +Event :: struct { + type: EventType, + + using _: struct #raw_union { + key: struct { + is_pressed: bool, + is_repeat: bool, + scancode: Scancode, + //code: Keycode, + mods: KeyMod, + }, + mouse: struct { + button: MouseButton, + using pos: Vec2, + scroll_direction: MouseScrollDirection, + }, + window: struct { + width: u32, + height: u32, + }, + } +} + +Mouse :: struct { + using pos: Vec2, + pressed: bool, + released: bool, + button: MouseButton, +} + +Window :: struct { + size: Vec2, + pre_fullscreen_size: Vec2, + is_fullscreen: bool, + non_resizable: bool, +} + +Keyboard :: struct { + mods: KeyMod, +} + +Context :: struct { + window_delete_atom: x11.Atom, + should_quit: bool, + screen_size: Vec2, + window: Window, + font: Font, + mouse: Mouse, + keyboard: Keyboard, + cursor: Cursor, + cursors: [Cursor]x11.Cursor, + ui: Ui, + + /* ZephrKeyboard keyboard; */ + /* XkbDescPtr xkb; */ + /* XIM xim; */ + /* CoreStack(ZephrEvent) event_queue; */ + /* u32 event_queue_cursor; */ + + projection: Mat4, +} + +@private +PropModeReplace :: 0 +@private +XA_ATOM :: x11.Atom(4) +@private +XA_STRING :: x11.Atom(31) +@private +XA_CARDINAL :: x11.Atom(6) +@private +FNV_HASH32_INIT :: 0x811c9dc5 +@private +FNV_HASH32_PRIME :: 0x01000193 + +COLOR_BLACK :: Color{0, 0, 0, 255} +COLOR_WHITE :: Color{255, 255, 255, 255} +COLOR_RED :: Color{255, 0, 0, 255} +COLOR_GREEN :: Color{0, 255, 0, 255} +COLOR_BLUE :: Color{0, 0, 255, 255} +COLOR_YELLOW :: Color{255, 255, 0, 255} +COLOR_MAGENTA :: Color{255, 0, 255, 255} +COLOR_CYAN :: Color{0, 255, 255, 255} +COLOR_ORANGE :: Color{255, 128, 0, 255} +COLOR_PURPLE :: Color{128, 0, 255, 255} + +evdev_scancode_to_zephr_scancode_map := []Scancode { + 0 = .NULL, + 1 = .ESCAPE, + 2 = .KEY_1, + 3 = .KEY_2, + 4 = .KEY_3, + 5 = .KEY_4, + 6 = .KEY_5, + 7 = .KEY_6, + 8 = .KEY_7, + 9 = .KEY_8, + 10 = .KEY_9, + 11 = .KEY_0, + 12 = .MINUS, + 13 = .EQUALS, + 14 = .BACKSPACE, + 15 = .TAB, + 16 = .Q, + 17 = .W, + 18 = .E, + 19 = .R, + 20 = .T, + 21 = .Y, + 22 = .U, + 23 = .I, + 24 = .O, + 25 = .P, + 26 = .LEFT_BRACKET, + 27 = .RIGHT_BRACKET, + 28 = .ENTER, + 29 = .LEFT_CTRL, + 30 = .A, + 31 = .S, + 32 = .D, + 33 = .F, + 34 = .G, + 35 = .H, + 36 = .J, + 37 = .K, + 38 = .L, + 39 = .SEMICOLON, + 40 = .APOSTROPHE, + 41 = .GRAVE, + 42 = .LEFT_SHIFT, + 43 = .BACKSLASH, + 44 = .Z, + 45 = .X, + 46 = .C, + 47 = .V, + 48 = .B, + 49 = .N, + 50 = .M, + 51 = .COMMA, + 52 = .PERIOD, + 53 = .SLASH, + 54 = .RIGHT_SHIFT, + 55 = .KP_MULTIPLY, + 56 = .LEFT_ALT, + 57 = .SPACE, + 58 = .CAPS_LOCK, + 59 = .F1, + 60 = .F2, + 61 = .F3, + 62 = .F4, + 63 = .F5, + 64 = .F6, + 65 = .F7, + 66 = .F8, + 67 = .F9, + 68 = .F10, + 69 = .NUM_LOCK_OR_CLEAR, + 70 = .SCROLL_LOCK, + 71 = .KP_7, + 72 = .KP_8, + 73 = .KP_9, + 74 = .KP_MINUS, + 75 = .KP_4, + 76 = .KP_5, + 77 = .KP_6, + 78 = .KP_PLUS, + 79 = .KP_1, + 80 = .KP_2, + 81 = .KP_3, + 82 = .KP_0, + 83 = .KP_PERIOD, + // 84 + 85 = .LANG5, // KEY_ZENKAKUHANKAKU + 86 = .NON_US_BACKSLASH, // KEY_102ND + 87 = .F11, + 88 = .F12, + 89 = .INTERNATIONAL1, // KEY_RO, + 90 = .LANG3, // KEY_KATAKANA + 91 = .LANG4, // KEY_HIRAGANA + 92 = .INTERNATIONAL4, // KEY_HENKAN + 93 = .INTERNATIONAL2, // KEY_KATAKANAHIRAGANA + 94 = .INTERNATIONAL5, // KEY_MUHENKAN + 95 = .INTERNATIONAL5, // KEY_KPJOCOMMA + 96 = .KP_ENTER, + 97 = .RIGHT_CTRL, + 98 = .KP_DIVIDE, + 99 = .SYSREQ, + 100 = .RIGHT_ALT, + 101 = .NULL, // KEY_LINEFEED + 102 = .HOME, + 103 = .UP, + 104 = .PAGE_UP, + 105 = .LEFT, + 106 = .RIGHT, + 107 = .END, + 108 = .DOWN, + 109 = .PAGE_DOWN, + 110 = .INSERT, + 111 = .DELETE, + 112 = .NULL, // KEY_MACRO + 113 = .MUTE, + 114 = .VOLUME_DOWN, + 115 = .VOLUME_UP, + 116 = .POWER, + 117 = .KP_EQUALS, + 118 = .KP_PLUS_MINUS, + 119 = .PAUSE, + // 120 + 121 = .KP_COMMA, + 122 = .LANG1, // KEY_HANGUEL + 123 = .LANG2, // KEY_HANJA + 124 = .INTERNATIONAL3, // KEY_YEN + 125 = .LEFT_META, + 126 = .RIGHT_META, + 127 = .APPLICATION, // KEY_COMPOSE + 128 = .STOP, + 129 = .AGAIN, + 130 = .NULL, // KEY_PROPS + 131 = .UNDO, + 132 = .NULL, // KEY_FRONT + 133 = .COPY, + 134 = .NULL, // KEY_OPEN + 135 = .PASTE, + 136 = .FIND, + 137 = .CUT, + 138 = .HELP, + 139 = .MENU, + 140 = .NULL, // CALCULATOR + 141 = .NULL, // KEY_SETUP + 142 = .NULL, // SLEEP + 143 = .NULL, // KEY_WAKEUP + 144 = .NULL, // KEY_FILE + 145 = .NULL, // KEY_SENDFILE + 146 = .NULL, // KEY_DELETEFILE + 147 = .NULL, // KEY_XFER + 148 = .NULL, // KEY_PROG1 + 149 = .NULL, // KEY_PROG2 + 150 = .NULL, // WWW + 151 = .NULL, // KEY_MSDOS + 152 = .NULL, // KEY_COFFEE + 153 = .NULL, // KEY_DIRECTION + 154 = .NULL, // KEY_CYCLEWINDOWS + 155 = .NULL, // MAIL + 156 = .NULL, // AC_BOOKMARKS + 157 = .NULL, // COMPUTER + 158 = .NULL, // AC_BACK + 159 = .NULL, // AC_FORWARD + 160 = .NULL, // KEY_CLOSECD + 161 = .NULL, // EJECT + 162 = .NULL, // KEY_EJECTCLOSECD + 163 = .NULL, // AUDIO_NEXT + 164 = .NULL, // AUDIO_PLAY + 165 = .NULL, // AUDIO_PREV + 166 = .NULL, // AUDIO_STOP + 167 = .NULL, // KEY_RECORD + 168 = .NULL, // AUDIO_REWIND + 169 = .NULL, // KEY_PHONE + 170 = .NULL, // KEY_ISO + 171 = .NULL, // KEY_CONFIG + 172 = .NULL, // AC_HOME + 173 = .NULL, // AC_REFRESH + 174 = .NULL, // KEY_EXIT + 175 = .NULL, // KEY_MOVE + 176 = .NULL, // KEY_EDIT + 177 = .NULL, // KEY_SCROLLUP + 178 = .NULL, // KEY_SCROLLDOWN + 179 = .KP_LEFT_PAREN, + 180 = .KP_RIGHT_PAREN, + 181 = .NULL, // KEY_NEW + 182 = .NULL, // KEY_REDO + 183 = .F13, + 184 = .F14, + 185 = .F15, + 186 = .F16, + 187 = .F17, + 188 = .F18, + 189 = .F19, + 190 = .F20, + 191 = .F21, + 192 = .F22, + 193 = .F23, + 194 = .F24, + // 195-199 + 200 = .NULL, // KEY_PLAYCD + 201 = .NULL, // KEY_PAUSECD + 202 = .NULL, // KEY_PROG3 + 203 = .NULL, // KEY_PROG4 + // 204 + 205 = .NULL, // KEY_SUSPEND + 206 = .NULL, // KEY_CLOSE + 207 = .NULL, // KEY_PLAY + 208 = .NULL, // AUDIO_FASTFORWARD + 209 = .NULL, // KEY_BASSBOOST + 210 = .NULL, // KEY_PRINT + 211 = .NULL, // KEY_HP + 212 = .NULL, // KEY_CAMERA + 213 = .NULL, // KEY_SOUND + 214 = .NULL, // KEY_QUESTION + 215 = .NULL, // KEY_EMAIL + 216 = .NULL, // KEY_CHAT + 217 = .NULL, // AC_SEARCH + 218 = .NULL, // KEY_CONNECT + 219 = .NULL, // KEY_FINANCE + 220 = .NULL, // KEY_SPORT + 221 = .NULL, // KEY_SHOP + 222 = .ALT_ERASE, + 223 = .CANCEL, + 224 = .NULL, // BRIGHTNESS_DOWN + 225 = .NULL, // BRIGHNESS_UP + 226 = .NULL, // KEY_MEDIA + 227 = .NULL, // DISPLAY_SWITCH + 228 = .NULL, // KBD_ILLUM_TOGGLE + 229 = .NULL, // KBD_ILLUM_DOWN + 230 = .NULL, // KBD_ILLUM_UP + 231 = .NULL, // KEY_SEND + 232 = .NULL, // KEY_REPLY + 233 = .NULL, // KEY_FORWARDEMAIL + 234 = .NULL, // KEY_SAVE + 235 = .NULL, // KEY_DOCUMENTS + 236 = .NULL, // KEY_BATTERY +} + +@private +x11_display : ^x11.Display +@private +x11_window : x11.Window +@private +x11_colormap : x11.Colormap +@private +glx_context : glx.Context +@private +zephr_ctx : Context + + +//////////////////////////// +// +// X11 +// +/////////////////////////// + + +@private +x11_go_fullscreen :: proc() { + // remove the resizing constraint before going fullscreen so WMs such as gnome + // can add the _NET_WM_ACTION_FULLSCREEN action to the _NET_WM_ALLOWED_ACTIONS atom + // and properly fullscreen the window + size_hints := x11.XAllocSizeHints(); + + zephr_ctx.window.pre_fullscreen_size.x = zephr_ctx.window.size.x; + zephr_ctx.window.pre_fullscreen_size.y = zephr_ctx.window.size.y; + if (size_hints != nil) { + size_hints.flags = {.PPosition, .PSize}; + size_hints.width = cast(i32)zephr_ctx.window.size.x; + size_hints.height = cast(i32)zephr_ctx.window.size.y; + x11.XSetWMNormalHints(x11_display, x11_window, size_hints); + x11.XFree(size_hints); + } + + xev: x11.XEvent; + wm_state := x11.XInternAtom(x11_display, "_NET_WM_STATE", false); + fullscreen := x11.XInternAtom(x11_display, "_NET_WM_STATE_FULLSCREEN", false); + xev.type = .ClientMessage; + xev.xclient.window = x11_window; + xev.xclient.message_type = wm_state; + xev.xclient.format = 32; + xev.xclient.data.l[0] = 1; // _NET_WM_STATE_ADD + xev.xclient.data.l[1] = cast(int)fullscreen; + xev.xclient.data.l[2] = 0; + x11.XSendEvent(x11_display, x11.XDefaultRootWindow(x11_display), false, + {.SubstructureNotify, .SubstructureRedirect}, &xev); +} + +@private +x11_return_fullscreen :: proc() { + xev: x11.XEvent; + wm_state := x11.XInternAtom(x11_display, "_NET_WM_STATE", false); + fullscreen := x11.XInternAtom(x11_display, "_NET_WM_STATE_FULLSCREEN", false); + xev.type = .ClientMessage; + xev.xclient.window = x11_window; + xev.xclient.message_type = wm_state; + xev.xclient.format = 32; + xev.xclient.data.l[0] = 0; // _NET_WM_STATE_REMOVE + xev.xclient.data.l[1] = cast(int)fullscreen; + xev.xclient.data.l[2] = 0; + x11.XSendEvent(x11_display, x11.XDefaultRootWindow(x11_display), false, + {.SubstructureNotify, .SubstructureRedirect}, &xev); + + // restore the resizing constraint as well as the pre-fullscreen window size + // when returning from fullscreen + size_hints := x11.XAllocSizeHints(); + + if (size_hints != nil) { + size_hints.flags = {.PPosition, .PSize}; + size_hints.width = cast(i32)zephr_ctx.window.pre_fullscreen_size.x; + size_hints.height = cast(i32)zephr_ctx.window.pre_fullscreen_size.y; + if (zephr_ctx.window.non_resizable) { + size_hints.flags |= {.PMinSize, .PMaxSize}; + size_hints.min_width = cast(i32)zephr_ctx.window.pre_fullscreen_size.x; + size_hints.min_height = cast(i32)zephr_ctx.window.pre_fullscreen_size.y; + size_hints.max_width = cast(i32)zephr_ctx.window.pre_fullscreen_size.x; + size_hints.max_height = cast(i32)zephr_ctx.window.pre_fullscreen_size.y; + } + x11.XSetWMNormalHints(x11_display, x11_window, size_hints); + x11.XFree(size_hints); + } +} + +@private +x11_toggle_fullscreen :: proc(fullscreen: bool) { + if fullscreen { + x11_return_fullscreen() + } else { + x11_go_fullscreen() + } +} + +// TODO: make sure this actually works ??? on gnome cuz I can't see it on dwm lol +// TODO: currently the icon shows on gnome's sidebar correctly but not in gnome-system-monitor. +// investigate. +@private +x11_assign_window_icon :: proc(icon_path: cstring) { + icon_width, icon_height: i32 + icon_data := image.load(icon_path, &icon_width, &icon_height, nil, 4); + defer image.image_free(icon_data) + assert(icon_data != nil, "Failed to load icon image") + + target_size := 2 + icon_width * icon_height; + + data := make([]u64, target_size * size_of(u64)) + + // first two elements are width and height + data[0] = cast(u64)icon_width; + data[1] = cast(u64)icon_height; + + for i in 0..<(icon_width*icon_height) { + data[i + 2] = cast(u64)((icon_data[i * 4] << 16) | (icon_data[i * 4 + 1] << 8) | (icon_data[i * 4 + 2] << 0) | (icon_data[i * 4 + 3] << 24)) + } + + net_wm_icon := x11.XInternAtom(x11_display, "_NET_WM_ICON", false); + + x11.XChangeProperty(x11_display, x11_window, net_wm_icon, XA_CARDINAL, 32, PropModeReplace, raw_data(data), target_size); +} + +@private +x11_get_screen_size :: proc() -> Vec2 { + screen := x11.XDefaultScreenOfDisplay(x11_display); + + return Vec2{cast(f32)screen.width, cast(f32)screen.height} +} + +@private +x11_resize_window :: proc() { + win_attrs : x11.XWindowAttributes + x11.XGetWindowAttributes(x11_display, x11_window, &win_attrs) + gl.Viewport(0, 0, win_attrs.width, win_attrs.height) +} + +@private +x11_create_window :: proc(window_title: cstring, window_size: Vec2, icon_path: cstring, window_non_resizable: bool) { + x11_display = x11.XOpenDisplay(nil) + + if x11_display == nil { + fmt.eprintf("Error: Can't open display\n") + return + } + + screen_num := x11.XDefaultScreen(x11_display) + root := x11.XRootWindow(x11_display, screen_num) + visual := x11.XDefaultVisual(x11_display, screen_num) + + x11_colormap = x11.XCreateColormap(x11_display, root, visual, x11.ColormapAlloc.AllocNone) + + attributes: x11.XSetWindowAttributes + attributes.event_mask = {.Exposure, .KeyPress, .KeyRelease, + .StructureNotify, .ButtonPress, .ButtonRelease, .PointerMotion} + attributes.colormap = x11_colormap + + screen := x11.XDefaultScreenOfDisplay(x11_display) + + window_start_x := screen.width / 2 - cast(i32)window_size.x / 2 + window_start_y := screen.height / 2 - cast(i32)window_size.y / 2 + + x11_window = x11.XCreateWindow(x11_display, root, window_start_x, window_start_y, cast(u32)window_size.x, cast(u32)window_size.y, 0, + x11.XDefaultDepth(x11_display), .InputOutput, visual, + {.CWColormap, .CWEventMask}, &attributes) + + if (icon_path != "") { + x11_assign_window_icon(icon_path); + } + + // Hints to the WM that the window is a normal window + // Of course this is only a hint and the WM can ignore it + net_wm_window_type := x11.XInternAtom(x11_display, "_NET_WM_WINDOW_TYPE", false) + net_wm_window_type_normal := x11.XInternAtom(x11_display, "_NET_WM_WINDOW_TYPE_NORMAL", false) + x11.XChangeProperty(x11_display, x11_window, net_wm_window_type, XA_ATOM, 32, PropModeReplace, &net_wm_window_type_normal, 1) + + wm_delete_window := x11.XInternAtom(x11_display, "WM_DELETE_WINDOW", false) + x11.XSetWMProtocols(x11_display, x11_window, &wm_delete_window, 1) + zephr_ctx.window_delete_atom = wm_delete_window; + + // set window name + { + UTF8_STRING := x11.XInternAtom(x11_display, "UTF8_STRING", false) + x11.XStoreName(x11_display, x11_window, window_title) + text_property: x11.XTextProperty + text_property.value = raw_data(string(window_title)) + text_property.format = 8 + text_property.encoding = UTF8_STRING + text_property.nitems = len(window_title) + x11.XSetWMName(x11_display, x11_window, &text_property) + net_wm_name := x11.XInternAtom(x11_display, "_NET_WM_NAME", false) + wm_class := x11.XInternAtom(x11_display, "WM_CLASS", false) + x11.XChangeProperty(x11_display, x11_window, net_wm_name, UTF8_STRING, 8, PropModeReplace, raw_data(string(window_title)), cast(i32)len(window_title)) + x11.XChangeProperty(x11_display, x11_window, wm_class, XA_STRING, 8, PropModeReplace, raw_data(string(window_title)), cast(i32)len(window_title)) + + class_hint := x11.XAllocClassHint() + + if (class_hint != nil) { + class_hint.res_name = window_title + class_hint.res_class = window_title + x11.XSetClassHint(x11_display, x11_window, class_hint) + x11.XFree(class_hint) + } + } + + size_hints := x11.XAllocSizeHints() + + if (size_hints != nil) { + size_hints.flags = {.PPosition, .PSize} + /* size_hints->win_gravity = StaticGravity; */ + size_hints.x = window_start_x + size_hints.y = window_start_y + size_hints.width = cast(i32)window_size.x + size_hints.height = cast(i32)window_size.y + if (window_non_resizable) { + size_hints.flags |= {.PMinSize, .PMaxSize}; + size_hints.min_width = cast(i32)window_size.x; + size_hints.min_height = cast(i32)window_size.y; + size_hints.max_width = cast(i32)window_size.x; + size_hints.max_height = cast(i32)window_size.y; + } + x11.XSetWMNormalHints(x11_display, x11_window, size_hints) + x11.XFree(size_hints) + } + + x11.XMapWindow(x11_display, x11_window) + + // nocheckin + gl.load_up_to(4, 6, proc(p: rawptr, name: cstring) { (cast(^rawptr)p)^ = glx.GetProcAddressARB(raw_data(string(name))) }) + + glx_major : i32 + glx_minor : i32 + glx.QueryVersion(x11_display, &glx_major, &glx_minor) + + fmt.printf("Loaded GLX %d.%d\n", glx_major, glx_minor) + + visual_attributes := []i32 { + glx.RENDER_TYPE, glx.RGBA_BIT, + glx.DEPTH_SIZE, 24, + glx.DOUBLEBUFFER, 1, + glx.SAMPLES, 4, // MSAA + x11.None, + } + + num_fbc: i32 + fbc := glx.ChooseFBConfig(x11_display, screen_num, raw_data(visual_attributes), &num_fbc) + + context_attributes := []i32 { + glx.CONTEXT_MAJOR_VERSION_ARB, 3, + glx.CONTEXT_MINOR_VERSION_ARB, 3, + glx.CONTEXT_PROFILE_MASK_ARB, glx.CONTEXT_CORE_PROFILE_BIT_ARB, + x11.None, + }; + + glx_context = glx.CreateContextAttribsARB(x11_display, fbc[0], nil, true, raw_data(context_attributes)); + + if glx_context == nil { + fmt.eprintf("Failed to create GLX context\n") + return + } + + glx.MakeCurrent(x11_display, x11_window, glx_context); + + // TODO: find a way to properly get the GL version + gl_major: i32 = --- + gl_minor: i32 = --- + gl.GetIntegerv(gl.MAJOR_VERSION, &gl_major) + gl.GetIntegerv(gl.MINOR_VERSION, &gl_major) + + fmt.printf("GL version: %d.%d\n", gl_major, gl_minor) + + glx.SwapIntervalEXT(x11_display, x11_window, 1); + // we enable blending for text + gl.Enable(gl.BLEND) + gl.Enable(gl.MULTISAMPLE) + gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) + x11_resize_window() + + x11.XFree(fbc) +} + +@private +x11_init :: proc(window_title: cstring, window_size: Vec2, icon_path: cstring, window_non_resizable: bool) { + x11_create_window(window_title, window_size, icon_path, window_non_resizable) +} + +@private +x11_close :: proc() { + glx.MakeCurrent(x11_display, 0, nil); + glx.DestroyContext(x11_display, glx_context); + + x11.XDestroyWindow(x11_display, x11_window); + x11.XFreeColormap(x11_display, x11_colormap); + x11.XCloseDisplay(x11_display); +} + + +//////////////////////////// +// +// Zephr +// +/////////////////////////// + + +init :: proc(font_path: cstring, icon_path: cstring, window_title: cstring, window_size: Vec2, window_non_resizable: bool) { + // TODO: should I initalize the audio here or let the game handle that?? + //int res = audio_init(); + //CORE_ASSERT(res == 0, "Failed to initialize audio"); + + x11_init(window_title, window_size, icon_path, window_non_resizable) + + ui_init(font_path) + + zephr_ctx.window.size = window_size + zephr_ctx.window.non_resizable = window_non_resizable + zephr_ctx.projection = orthographic_projection_2d(0, window_size.x, window_size.y, 0) + + zephr_ctx.cursors[.ARROW] = x11.XCreateFontCursor(x11_display, .XC_left_ptr); + zephr_ctx.cursors[.IBEAM] = x11.XCreateFontCursor(x11_display, .XC_xterm); + zephr_ctx.cursors[.CROSSHAIR] = x11.XCreateFontCursor(x11_display, .XC_crosshair); + zephr_ctx.cursors[.HAND] = x11.XCreateFontCursor(x11_display, .XC_hand1); + zephr_ctx.cursors[.HRESIZE] = x11.XCreateFontCursor(x11_display, .XC_sb_h_double_arrow); + zephr_ctx.cursors[.VRESIZE] = x11.XCreateFontCursor(x11_display, .XC_sb_v_double_arrow); + + // non-standard cursors + zephr_ctx.cursors[.DISABLED] = xcursor.LibraryLoadCursor(x11_display, "crossed_circle"); + + zephr_ctx.screen_size = x11_get_screen_size(); + start_internal_timer(); +} + +deinit :: proc() { + x11_close() + //audio_close() +} + +should_quit :: proc() -> bool { + gl.ClearColor(0, 0, 0, 1) + gl.Clear(gl.COLOR_BUFFER_BIT) + + zephr_ctx.cursor = .ARROW; + zephr_ctx.mouse.released = false; + zephr_ctx.mouse.pressed = false; + + //audio_update(); + + return zephr_ctx.should_quit; +} + +quit :: proc() { + zephr_ctx.should_quit = true; +} + +swap_buffers :: proc() { + if (zephr_ctx.ui.popup_open) { + draw_color_picker_popup(&zephr_ctx.ui.popup_parent_constraints); + } + zephr_ctx.ui.popup_open = false; + + glx.SwapBuffers(x11_display, x11_window) + x11.XDefineCursor(x11_display, x11_window, zephr_ctx.cursors[zephr_ctx.cursor]) +} + +iter_events :: proc(e_out: ^Event) -> bool { + xev: x11.XEvent + + for (cast(bool)x11.XPending(x11_display)) { + x11.XNextEvent(x11_display, &xev) + + if xev.type == .ConfigureNotify { + xce := xev.xconfigure + + if (xce.width != cast(i32)zephr_ctx.window.size.x || xce.height != cast(i32)zephr_ctx.window.size.y) { + zephr_ctx.window.size = Vec2{cast(f32)xce.width, cast(f32)xce.height} + zephr_ctx.projection = orthographic_projection_2d(0, zephr_ctx.window.size.x, zephr_ctx.window.size.y, 0) + x11_resize_window() + + e_out.type = .WINDOW_RESIZED + e_out.window.width = cast(u32)xce.width + e_out.window.height = cast(u32)xce.height + + return true + } + } else if xev.type == .DestroyNotify { + // window destroy event + e_out.type = .WINDOW_CLOSED; + + return true; + } else if xev.type == .ClientMessage { + // window close event + if (cast(x11.Atom)xev.xclient.data.l[0] == zephr_ctx.window_delete_atom) { + e_out.type = .WINDOW_CLOSED; + + return true; + } + } else if xev.type == .KeyPress { + xke := xev.xkey; + + evdev_keycode := xke.keycode - 8; + scancode := evdev_scancode_to_zephr_scancode_map[evdev_keycode]; + + e_out.type = .KEY_PRESSED; + e_out.key.scancode = scancode; + //e_out.key.code = keycode; + e_out.key.mods = x11_mods_to_zephr_mods(scancode, true); + + return true + } else if xev.type == .KeyRelease { + xke := xev.xkey; + + evdev_keycode := xke.keycode - 8; + scancode := evdev_scancode_to_zephr_scancode_map[evdev_keycode]; + + e_out.type = .KEY_RELEASED; + e_out.key.scancode = scancode + //e_out.key.code = keycode; + e_out.key.mods = x11_mods_to_zephr_mods(scancode, false); + + return true + } else if xev.type == .ButtonPress { + e_out.type = .MOUSE_BUTTON_PRESSED; + e_out.mouse.pos = Vec2{cast(f32)xev.xbutton.x, cast(f32)xev.xbutton.y}; + zephr_ctx.mouse.pressed = true; + + switch (xev.xbutton.button) { + case .Button1: + e_out.mouse.button = .BUTTON_LEFT; + zephr_ctx.mouse.button = .BUTTON_LEFT; + case .Button2: + e_out.mouse.button = .BUTTON_MIDDLE; + zephr_ctx.mouse.button = .BUTTON_MIDDLE; + case .Button3: + e_out.mouse.button = .BUTTON_RIGHT; + zephr_ctx.mouse.button = .BUTTON_RIGHT; + case .Button4: + e_out.type = .MOUSE_SCROLL; + e_out.mouse.scroll_direction = .UP; + case .Button5: + e_out.type = .MOUSE_SCROLL; + e_out.mouse.scroll_direction = .DOWN; + case cast(x11.MouseButton)8: // Back + e_out.mouse.button = .BUTTON_BACK; + zephr_ctx.mouse.button = .BUTTON_BACK; + case cast(x11.MouseButton)9: // Forward + e_out.mouse.button = .BUTTON_FORWARD; + zephr_ctx.mouse.button = .BUTTON_FORWARD; + case: + fmt.printf("[WARN] Unknown mouse button pressed: %d\n", xev.xbutton.button); + } + + return true + } else if xev.type == .ButtonRelease { + e_out.type = .MOUSE_BUTTON_RELEASED; + e_out.mouse.pos = Vec2{cast(f32)xev.xbutton.x, cast(f32)xev.xbutton.y}; + zephr_ctx.mouse.released = true; + zephr_ctx.mouse.pressed = false; + + switch (xev.xbutton.button) { + case .Button1: + e_out.mouse.button = .BUTTON_LEFT + case .Button2: + e_out.mouse.button = .BUTTON_MIDDLE + case .Button3: + e_out.mouse.button = .BUTTON_RIGHT + case .Button4: + e_out.type = .MOUSE_SCROLL; + e_out.mouse.scroll_direction = .UP; + case .Button5: + e_out.type = .MOUSE_SCROLL; + e_out.mouse.scroll_direction = .DOWN; + case cast(x11.MouseButton)8: // Back + e_out.mouse.button = .BUTTON_BACK; + zephr_ctx.mouse.button = .BUTTON_BACK; + case cast(x11.MouseButton)9: // Forward + e_out.mouse.button = .BUTTON_FORWARD; + zephr_ctx.mouse.button = .BUTTON_FORWARD; + } + + return true; + } else if xev.type == .MappingNotify { + // input device mapping changed + if (xev.xmapping.request != .MappingKeyboard) { + break; + } + x11.XRefreshKeyboardMapping(&xev.xmapping); + /* x11_keyboard_map_update(); */ + break; + } else if xev.type == .MotionNotify { + e_out.type = .MOUSE_MOVED; + e_out.mouse.pos = Vec2{cast(f32)xev.xmotion.x, cast(f32)xev.xmotion.y}; + zephr_ctx.mouse.pos = e_out.mouse.pos; + + return true; + } + } + + return false +} + +get_window_size :: proc() -> Vec2 { + return zephr_ctx.window.size; +} + +toggle_fullscreen :: proc() { + x11_toggle_fullscreen(zephr_ctx.window.is_fullscreen); + + zephr_ctx.window.is_fullscreen = !zephr_ctx.window.is_fullscreen; +} + +set_cursor :: proc(cursor: Cursor) { + zephr_ctx.cursor = cursor; +} + + +///////////////////////////// +// +// +// Utils +// +// +///////////////////////////// + + +@private +fnv_hash32 :: proc(data: []byte, size: u32, hash: u32) -> u32 { + hash := hash + + for i in 0.. KeyMod { + mods := zephr_ctx.keyboard.mods + + // TODO: figure out a better way of doing this + if (is_press) { + if (scancode == .LEFT_SHIFT) { + mods |= {.LEFT_SHIFT, .SHIFT}; + } + if (scancode == .RIGHT_SHIFT) { + mods |= {.RIGHT_SHIFT, .SHIFT}; + } + if (scancode == .LEFT_CTRL) { + mods |= {.LEFT_CTRL, .CTRL}; + } + if (scancode == .RIGHT_CTRL) { + mods |= {.RIGHT_CTRL, .CTRL}; + } + if (scancode == .LEFT_ALT) { + mods |= {.LEFT_ALT, .ALT}; + } + if (scancode == .RIGHT_ALT) { + mods |= {.RIGHT_ALT, .ALT}; + } + if (scancode == .LEFT_META) { + mods |= {.LEFT_META, .META}; + } + if (scancode == .RIGHT_META) { + mods |= {.RIGHT_META, .META}; + } + if (scancode == .CAPS_LOCK) { + mods |= {.CAPS_LOCK}; + } + if (scancode == .NUM_LOCK_OR_CLEAR) { + mods |= {.NUM_LOCK}; + } + } else { + if (scancode == .LEFT_SHIFT) { + mods &= ~{.LEFT_SHIFT}; + } + if (scancode == .RIGHT_SHIFT) { + mods &= ~{.RIGHT_SHIFT}; + } + if (!(.RIGHT_SHIFT in mods) && !(.LEFT_SHIFT in mods)) { + mods &= ~{.SHIFT}; + } + + if (scancode == .LEFT_CTRL) { + mods &= ~{.LEFT_CTRL}; + } + if (scancode == .RIGHT_CTRL) { + mods &= ~{.RIGHT_CTRL}; + } + if (!(.RIGHT_CTRL in mods) && !(.LEFT_CTRL in mods)) { + mods &= ~{.CTRL}; + } + + if (scancode == .LEFT_ALT) { + mods &= ~{.LEFT_ALT}; + } + if (scancode == .RIGHT_ALT) { + mods &= ~{.RIGHT_ALT}; + } + if (!(.RIGHT_ALT in mods) && !(.LEFT_ALT in mods)) { + mods &= ~{.ALT}; + } + + if (scancode == .LEFT_META) { + mods &= ~{.LEFT_META}; + } + if (scancode == .RIGHT_META) { + mods &= ~{.RIGHT_META}; + } + if (!(.RIGHT_META in mods) && !(.LEFT_META in mods)) { + mods &= ~{.META}; + } + + if (scancode == .CAPS_LOCK) { + mods &= ~{.CAPS_LOCK}; + } + if (scancode == .NUM_LOCK_OR_CLEAR) { + mods &= ~{.NUM_LOCK}; + } + } + + zephr_ctx.keyboard.mods = mods + return mods; +} diff --git a/zephr/zephr_math.odin b/zephr/zephr_math.odin new file mode 100644 index 0000000..93d1a92 --- /dev/null +++ b/zephr/zephr_math.odin @@ -0,0 +1,147 @@ +package zephr + +import "core:math" + +Vec2 :: [2]f32 +Vec3 :: [3]f32 +Vec4 :: [4]f32 + +Mat3 :: matrix[3, 3]f32 +Mat4 :: matrix[4, 4]f32 + +Color :: struct { r, g, b, a: u8 } + +orthographic_projection_2d :: proc(left, right, bottom, top: f32) -> Mat4 { + result := identity(); + + result[0][0] = 2 / (right - left); + result[3][0] = -(right + left) / (right - left); + result[1][1] = 2 / (top - bottom); + result[3][1] = -(top + bottom) / (top - bottom); + result[2][2] = -1; + result[3][3] = 1; + + return result; +} + +identity :: proc() -> Mat4 { + return Mat4 { + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1, + } +} + +translate :: proc(m: ^Mat4, v: Vec3) { + temp := identity() + + temp[3][0] = v.x + temp[3][1] = v.y + temp[3][2] = v.z + + m^ = temp * m^ +} + +rotate :: proc(m: ^Mat4, angle: f32, axis: Vec3) { + temp := identity() + + c := math.cos(math.to_radians(angle)) + s := math.sin(math.to_radians(angle)) + t := 1 - c + + x, y, z := axis.x, axis.y, axis.z + + res := Mat4{ + t*x*x + c, t*x*y - s*z, t*x*z + s*y, 0, + t*x*y + s*z, t*y*y + c, t*y*z - s*x, 0, + t*x*z - s*y, t*y*z + s*x, t*z*z + c, 0, + 0 , 0, 0, 1, + } + + m^ = res * m^ +} + +scale :: proc(m: ^Mat4, v: Vec3) { + temp := identity() + + temp[0][0] = v.x + temp[1][1] = v.y + temp[2][2] = v.z + + m^ = temp * m^ +} + +mult_color :: proc(color: Color, scalar: f32) -> Color { + color := color + + color.r = clamp(cast(u8)(cast(f32)color.r * scalar), 0, 255) + color.g = clamp(cast(u8)(cast(f32)color.g * scalar), 0, 255) + color.b = clamp(cast(u8)(cast(f32)color.b * scalar), 0, 255) + + return color +} + +hsv2rgb :: proc(h: f32, s: f32, v: f32) -> Color { + c := v * s; + x := (c * (1 - abs(math.mod(h / 60.0, 2) - 1))); + m := v - c; + + r, g, b: f32 + + if (h >= 0 && h < 60) { + r = c + g = x + b = 0 + } else if (h >= 60 && h < 120) { + r = x + g = c + b = 0 + } else if (h >= 120 && h < 180) { + r = 0 + g = c + b = x + } else if (h >= 180 && h < 240) { + r = 0 + g = x + b = c + } else if (h >= 240 && h < 300) { + r = x + g = 0 + b = c + } else { + r = c + g = 0 + b = x; + } + + return (Color){(u8)((r + m) * 255), (u8)((g + m) * 255), (u8)((b + m) * 255), 255}; +} + +determine_color_contrast :: proc(bg: Color) -> Color { + white_contrast := get_contrast(bg, COLOR_WHITE); + black_contrast := get_contrast(bg, COLOR_BLACK); + + return white_contrast > black_contrast ? COLOR_WHITE : COLOR_BLACK; +} + +@private +get_srgb :: proc(component: f32) -> f32 { + return (component / 255 <= 0.03928 + ? component / 255 / 12.92 + : math.pow((component / 255 + 0.055) / 1.055, 2.4)); +} + +get_luminance :: proc(color: Color) -> f32 { + return ((0.2126 * get_srgb(cast(f32)color.r)) + + (0.7152 * get_srgb(cast(f32)color.g)) + + (0.0722 * get_srgb(cast(f32)color.b))) / 255; +} + +@private +get_contrast :: proc(fg: Color, bg: Color) -> f32 { + l1 := get_luminance(fg) + l2 := get_luminance(bg) + + return (max(l1, l2) + 0.05) / (min(l1, l2) + 0.05); +}