diff --git a/api/server.zig b/api/server.zig index 7bf6953..1199ecc 100644 --- a/api/server.zig +++ b/api/server.zig @@ -26,68 +26,95 @@ const Image = Store.Image; const Server = @This(); -const RateLimitCategory = enum { - account_creation, - token, - data, - actions -}; +pub const RateLimit = struct { + pub const Category = enum { + account_creation, + token, + data, + actions + }; -const RateLimit = struct { - // NOTE: This could be refactor to remove the duplication between seconds, minutes and hours. - // Don't I feel that is TOO much duplication right now. So I will leave it be + pub const Timespan = struct { + counter: u32 = 0, + limit: u32, + timer_ms: u64 = 0, + }; - second_counter: u32 = 0, - minute_counter: u32 = 0, - hour_counter: u32 = 0, - - second_timer_ms: u64 = 0, - minute_timer_ms: u64 = 0, - hour_timer_ms: u64 = 0, - - limit_per_hour: ?u32 = null, - limit_per_minute: ?u32 = null, - limit_per_second: ?u32 = null, + seconds: ?Timespan = null, + minutes: ?Timespan = null, + hours: ?Timespan = null, last_update_at_ms: i64 = 0, - fn update_timers(self: *RateLimit) void { + pub fn init( + now: i64, + limit_per_hour: ?u32, + limit_per_minute: ?u32, + limit_per_second: ?u32 + ) RateLimit { + var seconds: ?Timespan = null; + if (limit_per_second) |limit| { + seconds = Timespan{ .limit = limit }; + } + + var minutes: ?Timespan = null; + if (limit_per_minute) |limit| { + minutes = Timespan{ .limit = limit }; + } + + var hours: ?Timespan = null; + if (limit_per_hour) |limit| { + hours = Timespan{ .limit = limit }; + } + + return RateLimit{ + .seconds = seconds, + .minutes = minutes, + .hours = hours, + .last_update_at_ms = now + }; + } + + pub fn update_timers(self: *RateLimit) void { const now = std.time.milliTimestamp(); const time_passed_ms = now - self.last_update_at_ms; assert(time_passed_ms >= 0); - if (self.limit_per_second != null) { - const seconds_passed: u32 = @truncate(@divFloor(self.second_timer_ms, std.time.ms_per_s)); - self.second_timer_ms += @intCast(time_passed_ms); - self.second_counter -= @min(self.second_counter, seconds_passed); - self.second_timer_ms = @mod(self.second_timer_ms, std.time.ms_per_min); - } + inline for (.{ + .{ &self.seconds, std.time.ms_per_s }, + .{ &self.minutes, std.time.ms_per_min }, + .{ &self.hours , std.time.ms_per_hour }, + }) |tuple| { + const maybe_timespan = tuple[0]; + const timespan_size = tuple[1]; - if (self.limit_per_minute != null) { - const minutes_passed: u32 = @truncate(@divFloor(self.second_timer_ms, std.time.ms_per_min)); - self.minute_timer_ms += @intCast(time_passed_ms); - self.minute_counter -= @min(self.minute_counter, minutes_passed); - self.minute_timer_ms = @mod(self.minute_timer_ms, std.time.ms_per_min); - } + if (maybe_timespan.*) |*timespan| { + timespan.timer_ms += @intCast(time_passed_ms); - if (self.limit_per_hour != null) { - const hours_passed: u32 = @truncate(@divFloor(self.second_timer_ms, std.time.ms_per_hour)); - self.hour_timer_ms += @intCast(time_passed_ms); - self.hour_counter -= @min(self.hour_counter, hours_passed); - self.hour_timer_ms = @mod(self.hour_timer_ms, std.time.ms_per_min); + const ms_per_request = @divFloor(timespan_size, timespan.limit); + const requests_passed: u32 = @intCast(@divFloor(timespan.timer_ms, ms_per_request)); + timespan.counter -= @min(timespan.counter, requests_passed); + timespan.timer_ms = @mod(timespan.timer_ms, ms_per_request); + } } self.last_update_at_ms = now; } - fn increment_counters(self: *RateLimit) void { - self.minute_counter += 1; - self.second_counter += 1; - self.hour_counter += 1; + pub fn increment_counters(self: *RateLimit) void { + inline for (.{ + &self.hours, + &self.minutes, + &self.seconds, + }) |maybe_timespan| { + if (maybe_timespan.*) |*timespan| { + timespan.counter += 1; + } + } } }; -const RateLimits = std.EnumArray(RateLimitCategory, RateLimit); +const RateLimits = std.EnumArray(RateLimit.Category, RateLimit); const max_response_size = 1024 * 1024 * 16; @@ -110,26 +137,10 @@ pub fn init(allocator: Allocator, store: *Store) !Server { // Limits gotten from https://docs.artifactsmmo.com/api_guide/rate_limits var ratelimits = RateLimits.initFill(RateLimit{}); - ratelimits.set(.account_creation, RateLimit{ - .last_update_at_ms = now, - .limit_per_hour = 50 - }); - ratelimits.set(.token, RateLimit{ - .last_update_at_ms = now, - .limit_per_hour = 50 - }); - ratelimits.set(.data, RateLimit{ - .last_update_at_ms = now, - .limit_per_second = 16, - .limit_per_minute = 200, - .limit_per_hour = 7200, - }); - ratelimits.set(.actions, RateLimit{ - .last_update_at_ms = now, - .limit_per_second = 5, - .limit_per_minute = 200, - .limit_per_hour = 7200, - }); + ratelimits.set(.account_creation, RateLimit.init(now, 50, null, null)); + ratelimits.set(.token, RateLimit.init(now, 50, null, null)); + ratelimits.set(.data, RateLimit.init(now, 7200, 200, 16)); + ratelimits.set(.actions, RateLimit.init(now, 7200, 200, 5)); return Server{ .client = .{ .allocator = allocator }, @@ -170,7 +181,7 @@ const FetchJsonOptions = struct { method: std.http.Method, path: []const u8, - ratelimit: RateLimitCategory, + ratelimit: RateLimit.Category, payload: ?[]const u8 = null, query: ?[]const QueryValue = null, @@ -209,7 +220,7 @@ fn maxIntBufferSize(comptime T: type) usize { return @max(max_int_size, min_int_size); } -fn fetch(self: *Server, ratelimit: RateLimitCategory, options: std.http.Client.FetchOptions) !std.http.Client.FetchResult { +fn fetch(self: *Server, ratelimit: RateLimit.Category, options: std.http.Client.FetchOptions) !std.http.Client.FetchResult { log.debug("+---- fetch -----", .{ }); log.debug("| endpoint {} {}", .{ options.method orelse .GET, options.location.uri }); if (options.payload) |payload| { diff --git a/gui/app.zig b/gui/app.zig index 4137b7f..b3432d5 100644 --- a/gui/app.zig +++ b/gui/app.zig @@ -3,9 +3,9 @@ const std = @import("std"); const Api = @import("artifacts-api"); const Artificer = @import("artificer"); const UI = @import("./ui.zig"); -const UIStack = @import("./ui_stack.zig"); -const RectUtils = @import("./rect_utils.zig"); +const RectUtils = @import("./rect-utils.zig"); const rl = @import("raylib"); +const FontFace = @import("./font-face.zig"); const srcery = @import("./srcery.zig"); const rlgl_h = @cImport({ @cInclude("rlgl.h"); @@ -27,6 +27,7 @@ map_texture_indexes: std.ArrayList(usize), map_position_min: Api.Position, map_position_max: Api.Position, camera: rl.Camera2D, +font_face: FontFace, blur_texture_original: ?rl.RenderTexture = null, blur_texture_horizontal: ?rl.RenderTexture = null, @@ -115,6 +116,15 @@ pub fn init(allocator: std.mem.Allocator, artificer: *Artificer) !App { return error.LoadShaderFromMemory; } + var fontChars: [95]i32 = undefined; + for (0..fontChars.len) |i| { + fontChars[i] = 32 + @as(i32, @intCast(i)); + } + var font = rl.loadFontFromMemory(".ttf", @embedFile("./roboto-font/Roboto-Medium.ttf"), 16, &fontChars); + if (!font.isReady()) { + return error.LoadFontFromMemory; + } + return App{ .artificer = artificer, .ui = UI.init(), @@ -123,6 +133,7 @@ pub fn init(allocator: std.mem.Allocator, artificer: *Artificer) !App { .map_position_max = map_position_max, .map_position_min = map_position_min, .blur_shader = blur_shader, + .font_face = .{ .font = font }, .camera = rl.Camera2D{ .offset = rl.Vector2.zero(), .target = rl.Vector2.zero(), @@ -137,9 +148,9 @@ pub fn deinit(self: *App) void { map_texture.texture.unload(); } - self.ui.deinit(); self.map_textures.deinit(); self.map_texture_indexes.deinit(); + self.font_face.font.unload(); if (self.blur_texture_horizontal) |render_texture| { render_texture.unload(); @@ -406,21 +417,21 @@ fn drawRectangleRoundedUV(rec: rl.Rectangle, roundness: f32, color: rl.Color) vo } } -fn drawBlurredWorld(self: *App, rect: rl.Rectangle, color: rl.Color) !void { - const blur_both = try createOrGetRenderTexture(&self.blur_texture_both); +fn drawBlurredWorld(self: *App, rect: rl.Rectangle, color: rl.Color) void { + const blur_both = self.blur_texture_both.?.texture; const previous_texture = rl.getShapesTexture(); const previous_rect = rl.getShapesTextureRectangle(); defer rl.setShapesTexture(previous_texture, previous_rect); - const texture_height: f32 = @floatFromInt(blur_both.texture.height); + const texture_height: f32 = @floatFromInt(blur_both.height); const shape_rect = rl.Rectangle{ .x = rect.x, .y = texture_height - rect.y, .width = rect.width, .height = -rect.height, }; - rl.setShapesTexture(blur_both.texture, shape_rect); + rl.setShapesTexture(blur_both, shape_rect); const border = 2; const roundness = 0.2; @@ -586,8 +597,78 @@ pub fn drawWorldAndBlur(self: *App) !void { ); } +fn drawRatelimits(self: *App, box: rl.Rectangle) void { + const Category = Api.Server.RateLimit.Category; + const ratelimits = self.artificer.server.ratelimits; + + self.drawBlurredWorld( + box, + srcery.xgray10 + ); + + const padding = 16; + var stack = UI.Stack.init(RectUtils.shrink(box, padding, padding), .top_to_bottom); + stack.gap = 8; + + inline for (.{ + .{ "Account creation", Category.account_creation }, + .{ "Token", Category.token }, + .{ "Data", Category.data }, + .{ "Actions", Category.actions }, + }) |ratelimit_bar| { + const title = ratelimit_bar[0]; + const category = ratelimit_bar[1]; + const ratelimit = ratelimits.get(category); + + const ratelimit_box = stack.next(24); + rl.drawRectangleRec(ratelimit_box, rl.Color.white); + + inline for (.{ + .{ ratelimit.hours , std.time.ms_per_hour, srcery.red }, + .{ ratelimit.minutes, std.time.ms_per_min , srcery.blue }, + .{ ratelimit.seconds, std.time.ms_per_s , srcery.green }, + }) |limit_spec| { + const maybe_timespan = limit_spec[0]; + const timespan_size = limit_spec[1]; + const color = limit_spec[2]; + + if (maybe_timespan) |timespan| { + const limit_f32: f32 = @floatFromInt(timespan.limit); + const counter_f32: f32 = @floatFromInt(timespan.counter); + const timer_f32: f32 = @floatFromInt(timespan.timer_ms); + + const ms_per_request = timespan_size / limit_f32; + const progress = std.math.clamp((counter_f32 - timer_f32 / ms_per_request) / limit_f32, 0, 1); + + var progress_bar = ratelimit_box; + progress_bar.width *= progress; + rl.drawRectangleRec(progress_bar, color); + } + } + + if (self.ui.isMouseInside(ratelimit_box)) { + // TODO: Draw more detailed info about rate limits. + // Show how many requests have occured + } else { + const title_size = self.font_face.measureText(title); + self.font_face.drawText( + title, + .{ + .x = ratelimit_box.x + 8, + .y = ratelimit_box.y + title_size.y/2, + }, + srcery.white + ); + } + } +} + pub fn tick(self: *App) !void { + var server = self.artificer.server; try self.artificer.tick(); + for (&server.ratelimits.values) |*ratelimit| { + ratelimit.update_timers(); + } const screen_width = rl.getScreenWidth(); const screen_height = rl.getScreenHeight(); @@ -602,9 +683,8 @@ pub fn tick(self: *App) !void { try self.drawWorldAndBlur(); - try self.drawBlurredWorld( + self.drawRatelimits( .{ .x = 20, .y = 20, .width = 200, .height = 200 }, - srcery.xgray10 ); rl.drawFPS( diff --git a/gui/font-face.zig b/gui/font-face.zig new file mode 100644 index 0000000..9504e1c --- /dev/null +++ b/gui/font-face.zig @@ -0,0 +1,138 @@ +const std = @import("std"); +const rl = @import("raylib"); + +font: rl.Font, +spacing: ?f32 = null, +line_height: f32 = 1.4, + +pub fn getSpacing(self: @This()) f32 { + if (self.spacing) |spacing| { + return spacing; + } else { + return self.getSize() / 10; + } +} + +pub fn getSize(self: @This()) f32 { + return @floatFromInt(self.font.baseSize); +} + +pub fn drawTextLines(self: @This(), lines: []const []const u8, position: rl.Vector2, tint: rl.Color) void { + var offset_y: f32 = 0; + + const font_size = self.getSize(); + + for (lines) |line| { + self.drawText(line, position.add(.{ .x = 0, .y = offset_y }), tint); + + const line_size = self.measureText(line); + offset_y += line_size.y + font_size * (self.line_height - 1); + } +} + +pub fn measureTextLines(self: @This(), lines: []const []const u8) rl.Vector2 { + var text_size = rl.Vector2.zero(); + + const font_size = self.getSize(); + + for (lines) |line| { + const line_size = self.measureText(line); + + text_size.x = @max(text_size.x, line_size.x); + text_size.y += line_size.y; + } + + text_size.y += (self.line_height - 1) * font_size * @as(f32, @floatFromInt(@max(lines.len - 1, 0))); + + return text_size; +} + +pub fn drawTextEx(self: @This(), text: []const u8, position: rl.Vector2, tint: rl.Color, offset: *rl.Vector2) void { + if (self.font.texture.id == 0) return; + + const font_size = self.getSize(); + const spacing = self.getSpacing(); + + var iter = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 }; + while (iter.nextCodepoint()) |codepoint| { + if (codepoint == '\n') { + offset.x = 0; + offset.y += font_size * self.line_height; + } else { + if (!(codepoint <= 255 and std.ascii.isWhitespace(@intCast(codepoint)))) { + var codepoint_position = position.add(offset.*); + codepoint_position.x = @round(codepoint_position.x); + codepoint_position.y = @round(codepoint_position.y); + rl.drawTextCodepoint(self.font, codepoint, codepoint_position, font_size, tint); + } + + const index: u32 = @intCast(rl.getGlyphIndex(self.font, codepoint)); + if (self.font.glyphs[index].advanceX != 0) { + offset.x += @floatFromInt(self.font.glyphs[index].advanceX); + } else { + offset.x += self.font.recs[index].width; + offset.x += @floatFromInt(self.font.glyphs[index].offsetX); + } + offset.x += spacing; + } + } +} + +pub fn drawText(self: @This(), text: []const u8, position: rl.Vector2, tint: rl.Color) void { + var offset = rl.Vector2.init(0, 0); + self.drawTextEx(text, position, tint, &offset); +} + +pub fn drawTextAlloc(self: @This(), allocator: std.mem.Allocator, comptime fmt: []const u8, args: anytype, position: rl.Vector2, tint: rl.Color) !void { + const text = try std.fmt.allocPrint(allocator, fmt, args); + defer allocator.free(text); + + self.drawText(text, position, tint); +} + +pub fn measureText(self: @This(), text: []const u8) rl.Vector2 { + var text_size = rl.Vector2.zero(); + + if (self.font.texture.id == 0) return text_size; // Security check + if (text.len == 0) return text_size; + + const font_size = self.getSize(); + const spacing = self.getSpacing(); + + var line_width: f32 = 0; + text_size.y = font_size; + + var iter = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 }; + while (iter.nextCodepoint()) |codepoint| { + if (codepoint == '\n') { + text_size.y += font_size * self.line_height; + + line_width = 0; + } else { + if (line_width > 0) { + line_width += spacing; + } + + const index: u32 = @intCast(rl.getGlyphIndex(self.font, codepoint)); + if (self.font.glyphs[index].advanceX != 0) { + line_width += @floatFromInt(self.font.glyphs[index].advanceX); + } else { + line_width += self.font.recs[index].width; + line_width += @floatFromInt(self.font.glyphs[index].offsetX); + } + + text_size.x = @max(text_size.x, line_width); + } + } + + return text_size; +} + +pub fn drawTextCenter(self: @This(), text: []const u8, position: rl.Vector2, tint: rl.Color) void { + const text_size = self.measureText(text); + const adjusted_position = rl.Vector2{ + .x = position.x - text_size.x / 2, + .y = position.y - text_size.y / 2, + }; + self.drawText(text, adjusted_position, tint); +} diff --git a/gui/rect_utils.zig b/gui/rect-utils.zig similarity index 100% rename from gui/rect_utils.zig rename to gui/rect-utils.zig diff --git a/gui/roboto-font/LICENSE.txt b/gui/roboto-font/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/gui/roboto-font/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/gui/roboto-font/Roboto-Black.ttf b/gui/roboto-font/Roboto-Black.ttf new file mode 100644 index 0000000..58fa175 Binary files /dev/null and b/gui/roboto-font/Roboto-Black.ttf differ diff --git a/gui/roboto-font/Roboto-BlackItalic.ttf b/gui/roboto-font/Roboto-BlackItalic.ttf new file mode 100644 index 0000000..0a4dfd0 Binary files /dev/null and b/gui/roboto-font/Roboto-BlackItalic.ttf differ diff --git a/gui/roboto-font/Roboto-Bold.ttf b/gui/roboto-font/Roboto-Bold.ttf new file mode 100644 index 0000000..e64db79 Binary files /dev/null and b/gui/roboto-font/Roboto-Bold.ttf differ diff --git a/gui/roboto-font/Roboto-BoldItalic.ttf b/gui/roboto-font/Roboto-BoldItalic.ttf new file mode 100644 index 0000000..5e39ae9 Binary files /dev/null and b/gui/roboto-font/Roboto-BoldItalic.ttf differ diff --git a/gui/roboto-font/Roboto-Italic.ttf b/gui/roboto-font/Roboto-Italic.ttf new file mode 100644 index 0000000..65498ee Binary files /dev/null and b/gui/roboto-font/Roboto-Italic.ttf differ diff --git a/gui/roboto-font/Roboto-Light.ttf b/gui/roboto-font/Roboto-Light.ttf new file mode 100644 index 0000000..a7e0284 Binary files /dev/null and b/gui/roboto-font/Roboto-Light.ttf differ diff --git a/gui/roboto-font/Roboto-LightItalic.ttf b/gui/roboto-font/Roboto-LightItalic.ttf new file mode 100644 index 0000000..867b76d Binary files /dev/null and b/gui/roboto-font/Roboto-LightItalic.ttf differ diff --git a/gui/roboto-font/Roboto-Medium.ttf b/gui/roboto-font/Roboto-Medium.ttf new file mode 100644 index 0000000..0707e15 Binary files /dev/null and b/gui/roboto-font/Roboto-Medium.ttf differ diff --git a/gui/roboto-font/Roboto-MediumItalic.ttf b/gui/roboto-font/Roboto-MediumItalic.ttf new file mode 100644 index 0000000..4e3bf0d Binary files /dev/null and b/gui/roboto-font/Roboto-MediumItalic.ttf differ diff --git a/gui/roboto-font/Roboto-Regular.ttf b/gui/roboto-font/Roboto-Regular.ttf new file mode 100644 index 0000000..2d116d9 Binary files /dev/null and b/gui/roboto-font/Roboto-Regular.ttf differ diff --git a/gui/roboto-font/Roboto-Thin.ttf b/gui/roboto-font/Roboto-Thin.ttf new file mode 100644 index 0000000..ab68508 Binary files /dev/null and b/gui/roboto-font/Roboto-Thin.ttf differ diff --git a/gui/roboto-font/Roboto-ThinItalic.ttf b/gui/roboto-font/Roboto-ThinItalic.ttf new file mode 100644 index 0000000..b2c3933 Binary files /dev/null and b/gui/roboto-font/Roboto-ThinItalic.ttf differ diff --git a/gui/ui.zig b/gui/ui.zig index e05ae08..11d9f8d 100644 --- a/gui/ui.zig +++ b/gui/ui.zig @@ -1,159 +1,200 @@ -const rl = @import("raylib"); const std = @import("std"); +const rl = @import("raylib"); +const rect_utils = @import("./rect-utils.zig"); +const assert = std.debug.assert; +const SourceLocation = std.builtin.SourceLocation; + +// TODO: Implement Id context (I.e. ID parenting) const UI = @This(); -font: rl.Font, +const max_stack_depth = 16; +const TransformFrame = struct { + offset: rl.Vector2, + scale: rl.Vector2, +}; + +hot_widget: ?Id = null, +active_widget: ?Id = null, + +transform_stack: std.BoundedArray(TransformFrame, max_stack_depth), pub fn init() UI { + var stack = std.BoundedArray(TransformFrame, max_stack_depth).init(0) catch unreachable; + stack.appendAssumeCapacity(TransformFrame{ + .offset = rl.Vector2{ .x = 0, .y = 0 }, + .scale = rl.Vector2{ .x = 1, .y = 1 }, + }); + return UI{ - .font = rl.getFontDefault() + .transform_stack = stack }; } -pub fn deinit(self: UI) void { - rl.unloadFont(self.font); +pub fn isHot(self: *const UI, id: Id) bool { + if (self.hot_widget) |hot_id| { + return hot_id.eql(id); + } + return false; } -// Reimplementation of `GetGlyphIndex` from raylib in src/rtext.c -fn GetGlyphIndex(font: rl.Font, codepoint: i32) usize { - var index: usize = 0; +pub fn isActive(self: *const UI, id: Id) bool { + if (self.active_widget) |active_id| { + return active_id.eql(id); + } + return false; +} - var fallbackIndex: usize = 0; // Get index of fallback glyph '?' +pub fn hashSrc(src: SourceLocation) u64 { + var hash = std.hash.Fnv1a_64.init(); + hash.update(src.file); + hash.update(std.mem.asBytes(&src.line)); + hash.update(std.mem.asBytes(&src.column)); + return hash.value; +} - for (0..@intCast(font.glyphCount), font.glyphs) |i, glyph| { - if (glyph.value == '?') fallbackIndex = i; +fn getTopFrame(self: *UI) *TransformFrame { + assert(self.transform_stack.len >= 1); + return &self.transform_stack.buffer[self.transform_stack.len-1]; +} - if (glyph.value == codepoint) - { - index = i; - break; - } +pub fn getMousePosition(self: *UI) rl.Vector2 { + const frame = self.getTopFrame(); + return rl.getMousePosition().subtract(frame.offset).divide(frame.scale); +} + +pub fn getMouseDelta(self: *UI) rl.Vector2 { + const frame = self.getTopFrame(); + return rl.Vector2.multiply(rl.getMouseDelta(), frame.scale); +} + +pub fn getMouseWheelMove(self: *UI) f32 { + const frame = self.getTopFrame(); + return rl.getMouseWheelMove() * frame.scale.y; +} + +pub fn isMouseInside(self: *UI, rect: rl.Rectangle) bool { + return rect_utils.isInsideVec2(rect, self.getMousePosition()); +} + +pub fn transformScale(self: *UI, x: f32, y: f32) void { + const frame = self.getTopFrame(); + frame.scale.x *= x; + frame.scale.y *= y; + + rl.gl.rlScalef(x, y, 1); +} + +pub fn transformTranslate(self: *UI, x: f32, y: f32) void { + const frame = self.getTopFrame(); + frame.offset.x += x * frame.scale.x; + frame.offset.y += y * frame.scale.y; + + rl.gl.rlTranslatef(x, y, 0); +} + +pub fn pushTransform(self: *UI) void { + rl.gl.rlPushMatrix(); + self.transform_stack.appendAssumeCapacity(self.getTopFrame().*); +} + +pub fn popTransform(self: *UI) void { + assert(self.transform_stack.len >= 2); + rl.gl.rlPopMatrix(); + _ = self.transform_stack.pop(); +} + +pub fn beginScissorMode(self: *UI, x: f32, y: f32, width: f32, height: f32) void { + const frame = self.getTopFrame(); + + rl.beginScissorMode( + @intFromFloat(x * frame.scale.x + frame.offset.x), + @intFromFloat(y * frame.scale.y + frame.offset.y), + @intFromFloat(width * frame.scale.x), + @intFromFloat(height * frame.scale.y), + ); +} + +pub fn beginScissorModeRect(self: *UI, rect: rl.Rectangle) void { + self.beginScissorMode(rect.x, rect.y, rect.width, rect.height); +} + +pub fn endScissorMode(self: *UI) void { + _ = self; + rl.endScissorMode(); +} + +pub const Id = struct { + location: u64, + extra: u32 = 0, + + pub fn init(comptime src: SourceLocation) Id { + return Id{ .location = comptime hashSrc(src) }; } - if ((index == 0) and (font.glyphs[0].value != codepoint)) index = fallbackIndex; - - return index; -} - -fn GetCodePointNext(text: []const u8, next: *usize) i32 { - var letter: i32 = '?'; - - if (std.unicode.utf8ByteSequenceLength(text[0])) |codepointSize| { - next.* = codepointSize; - if (std.unicode.utf8Decode(text[0..codepointSize])) |codepoint| { - letter = @intCast(codepoint); - } else |_| {} - } else |_| {} - - return letter; -} - -// NOTE: Line spacing is a global variable, use SetTextLineSpacing() to setup -const textLineSpacing = 2; // TODO: Assume that line spacing is not changed. - -// Reimplementation of `rl.drawTextEx`, so a null terminated would not be required -pub fn drawTextEx(font: rl.Font, text: []const u8, position: rl.Vector2, font_size: f32, spacing: f32, tint: rl.Color) void { - var used_font = font; - if (font.texture.id == 0) { - used_font = rl.getFontDefault(); + pub fn eql(a: Id, b: Id) bool { + return a.location == b.location and a.extra == b.extra; } +}; - var text_offset_y: f32 = 0; - var text_offset_x: f32 = 0; - - const scale_factor = font_size / @as(f32, @floatFromInt(used_font.baseSize)); - - var i: usize = 0; - while (i < text.len) { - var next: usize = 0; - - const letter = GetCodePointNext(text[i..], &next); - const index = GetGlyphIndex(font, letter); - - i += next; - - if (letter == '\n') { - text_offset_x = 0; - text_offset_y += (font_size + textLineSpacing); - } else { - if (letter != ' ' and letter != '\t') { - rl.drawTextCodepoint(font, letter, .{ - .x = position.x + text_offset_x, - .y = position.y + text_offset_y, - }, font_size, tint); - } - - if (font.glyphs[index].advanceX == 0) { - text_offset_x += font.recs[index].width*scale_factor + spacing; - } else { - text_offset_x += @as(f32, @floatFromInt(font.glyphs[index].advanceX))*scale_factor + spacing; - } - } - } -} - -// Reimplementation of `rl.measureTextEx`, so a null terminated would not be required -pub fn measureTextEx(font: rl.Font, text: []const u8, fontSize: f32, spacing: f32) rl.Vector2 { - var textSize = rl.Vector2.init(0, 0); - - if (font.texture.id == 0) return textSize; // Security check - - var tempByteCounter: i32 = 0; // Used to count longer text line num chars - var byteCounter: i32 = 0; - - var textWidth: f32 = 0; - var tempTextWidth: f32 = 0; // Used to count longer text line width - - var textHeight: f32 = fontSize; - const scaleFactor: f32 = fontSize/@as(f32, @floatFromInt(font.baseSize)); - - var i: usize = 0; - while (i < text.len) - { - byteCounter += 1; - - var next: usize = 0; - - const letter = GetCodePointNext(text[i..], &next); - const index = GetGlyphIndex(font, letter); - - i += next; - - if (letter != '\n') - { - if (font.glyphs[index].advanceX != 0) { - textWidth += @floatFromInt(font.glyphs[index].advanceX); - } else { - textWidth += font.recs[index].width; - textWidth += @floatFromInt(font.glyphs[index].offsetX); - } - } - else - { - if (tempTextWidth < textWidth) tempTextWidth = textWidth; - byteCounter = 0; - textWidth = 0; - - textHeight += (fontSize + textLineSpacing); - } - - if (tempByteCounter < byteCounter) tempByteCounter = byteCounter; - } - - if (tempTextWidth < textWidth) tempTextWidth = textWidth; - - textSize.x = tempTextWidth*scaleFactor + @as(f32, @floatFromInt(tempByteCounter - 1)) * spacing; - textSize.y = textHeight; - - return textSize; -} - -pub fn drawTextCentered(font: rl.Font, text: []const u8, position: rl.Vector2, font_size: f32, spacing: f32, color: rl.Color) void { - const text_size = measureTextEx(font, text, font_size, spacing); - const adjusted_position = rl.Vector2{ - .x = position.x - text_size.x/2, - .y = position.y - text_size.y/2, +pub const Stack = struct { + pub const Direction = enum { + top_to_bottom, + bottom_to_top, + left_to_right }; - drawTextEx(font, text, adjusted_position, font_size, spacing, color); -} + + unused_box: rl.Rectangle, + dir: Direction, + gap: f32 = 0, + + pub fn init(box: rl.Rectangle, dir: Direction) Stack { + return Stack{ + .unused_box = box, + .dir = dir + }; + } + + pub fn next(self: *Stack, size: f32) rl.Rectangle { + return switch (self.dir) { + .top_to_bottom => { + const next_box = rl.Rectangle.init(self.unused_box.x, self.unused_box.y, self.unused_box.width, size); + self.unused_box.y += size; + self.unused_box.y += self.gap; + return next_box; + }, + .bottom_to_top => { + const next_box = rl.Rectangle.init(self.unused_box.x, self.unused_box.y + self.unused_box.height - size, self.unused_box.width, size); + self.unused_box.height -= size; + self.unused_box.height -= self.gap; + return next_box; + }, + .left_to_right => { + const next_box = rl.Rectangle.init(self.unused_box.x, self.unused_box.y, size, self.unused_box.height); + self.unused_box.x += size; + self.unused_box.x += self.gap; + return next_box; + }, + }; + } +}; + +pub const IdIterator = struct { + id: Id, + counter: u32, + + pub fn init(comptime src: SourceLocation) IdIterator { + return IdIterator{ + .id = Id.init(src), + .counter = 0 + }; + } + + pub fn next(self: *IdIterator) Id { + var id = self.id; + id.extra = self.counter; + + self.counter += 1; + return id; + } +}; diff --git a/gui/ui_stack.zig b/gui/ui_stack.zig deleted file mode 100644 index 90bec37..0000000 --- a/gui/ui_stack.zig +++ /dev/null @@ -1,42 +0,0 @@ -const rl = @import("raylib"); -const Stack = @This(); - -pub const Direction = enum { - top_to_bottom, - bottom_to_top, - left_to_right -}; - -unused_box: rl.Rectangle, -dir: Direction, -gap: f32 = 0, - -pub fn init(box: rl.Rectangle, dir: Direction) Stack { - return Stack{ - .unused_box = box, - .dir = dir - }; -} - -pub fn next(self: *Stack, size: f32) rl.Rectangle { - return switch (self.dir) { - .top_to_bottom => { - const next_box = rl.Rectangle.init(self.unused_box.x, self.unused_box.y, self.unused_box.width, size); - self.unused_box.y += size; - self.unused_box.y += self.gap; - return next_box; - }, - .bottom_to_top => { - const next_box = rl.Rectangle.init(self.unused_box.x, self.unused_box.y + self.unused_box.height - size, self.unused_box.width, size); - self.unused_box.height -= size; - self.unused_box.height -= self.gap; - return next_box; - }, - .left_to_right => { - const next_box = rl.Rectangle.init(self.unused_box.x, self.unused_box.y, size, self.unused_box.height); - self.unused_box.x += size; - self.unused_box.x += self.gap; - return next_box; - }, - }; -}