ui progress
This commit is contained in:
parent
fad9f779b6
commit
576df80400
266
src/app.zig
266
src/app.zig
@ -2,6 +2,7 @@ const std = @import("std");
|
|||||||
const log = std.log.scoped(.app);
|
const log = std.log.scoped(.app);
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
const STBImage = @import("stb_image");
|
const STBImage = @import("stb_image");
|
||||||
const STBTrueType = @import("stb_truetype");
|
const STBTrueType = @import("stb_truetype");
|
||||||
@ -18,6 +19,7 @@ const Color = Platform.Color;
|
|||||||
const CanvasScaler = @import("./canvas_scaler.zig");
|
const CanvasScaler = @import("./canvas_scaler.zig");
|
||||||
const Font = @import("./font.zig");
|
const Font = @import("./font.zig");
|
||||||
const Tilesheet = @import("./tilesheet.zig");
|
const Tilesheet = @import("./tilesheet.zig");
|
||||||
|
const UI = @import("./ui.zig");
|
||||||
|
|
||||||
const App = @This();
|
const App = @This();
|
||||||
|
|
||||||
@ -332,9 +334,14 @@ const BoxSprites = struct {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
world: World,
|
const GameScene = struct {
|
||||||
undo_stack: std.ArrayList(UndoFrame) = .empty,
|
world: World,
|
||||||
current_level: u32 = 0,
|
undo_stack: std.ArrayList(UndoFrame) = .empty,
|
||||||
|
current_level: u32 = 0,
|
||||||
|
animation_timer: Platform.Nanoseconds = 0,
|
||||||
|
paused: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
levels: std.ArrayList(World),
|
levels: std.ArrayList(World),
|
||||||
|
|
||||||
roboto_font: Font,
|
roboto_font: Font,
|
||||||
@ -342,13 +349,16 @@ tilesheet: Tilesheet,
|
|||||||
|
|
||||||
player_sprites: PlayerSprites,
|
player_sprites: PlayerSprites,
|
||||||
box_sprites_by_color: std.EnumArray(BoxColor, BoxSprites),
|
box_sprites_by_color: std.EnumArray(BoxColor, BoxSprites),
|
||||||
animation_timer: Platform.Nanoseconds,
|
|
||||||
|
|
||||||
footstep_buffer: Audio.BufferId,
|
footstep_buffer: Audio.BufferId,
|
||||||
footstep_sound: Audio.SoundId = .nil,
|
footstep_sound: Audio.SoundId = .nil,
|
||||||
|
|
||||||
sfx_bus: Audio.BusId,
|
sfx_bus: Audio.BusId,
|
||||||
|
|
||||||
|
game_scene: GameScene,
|
||||||
|
in_main_menu: bool = false,
|
||||||
|
main_menu_state: UI.State = .empty,
|
||||||
|
|
||||||
pub fn init(self: *App, plt: Platform.Init) !?u8 {
|
pub fn init(self: *App, plt: Platform.Init) !?u8 {
|
||||||
const roboto_font = try Font.init(plt.gpa, .{
|
const roboto_font = try Font.init(plt.gpa, .{
|
||||||
.spritesheet = Gfx.initSpritesheet(.{ .format = .r8 }),
|
.spritesheet = Gfx.initSpritesheet(.{ .format = .r8 }),
|
||||||
@ -409,7 +419,6 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
|
|||||||
self.* = App{
|
self.* = App{
|
||||||
.player_sprites = player_sprites,
|
.player_sprites = player_sprites,
|
||||||
.roboto_font = roboto_font,
|
.roboto_font = roboto_font,
|
||||||
.animation_timer = 0,
|
|
||||||
.tilesheet = tilesheet,
|
.tilesheet = tilesheet,
|
||||||
.footstep_buffer = footstep_sound,
|
.footstep_buffer = footstep_sound,
|
||||||
.box_sprites_by_color = .init(.{
|
.box_sprites_by_color = .init(.{
|
||||||
@ -441,12 +450,15 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
.sfx_bus = Audio.addBus(.{ .label = "sfx" }),
|
.sfx_bus = Audio.addBus(.{ .label = "sfx" }),
|
||||||
.world = undefined,
|
|
||||||
.levels = .empty,
|
.levels = .empty,
|
||||||
|
|
||||||
|
.game_scene = .{
|
||||||
|
.world = undefined,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
self.levels = try self.loadLevels(plt.gpa, plt.assets);
|
self.levels = try self.loadLevels(plt.gpa, plt.assets);
|
||||||
self.world.copy(self.levels.items[0]);
|
self.game_scene.world.copy(self.levels.items[0]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -544,6 +556,7 @@ fn loadLevel(
|
|||||||
.x = @intCast(ox),
|
.x = @intCast(ox),
|
||||||
.y = @intCast(oy),
|
.y = @intCast(oy),
|
||||||
},
|
},
|
||||||
|
.visual_pos = .initFromInt(usize, ox, oy),
|
||||||
.sprite = self.tilesheet.get(pos.x, pos.y)
|
.sprite = self.tilesheet.get(pos.x, pos.y)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -629,42 +642,100 @@ fn getPlayerInput(input: *Platform.Input) PlayerInput {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn frame(self: *App, plt: Platform.Frame) !void {
|
const clear_color = Color.rgb(40, 40, 40);
|
||||||
const input = getPlayerInput(plt.input);
|
const tile_size = Vec2.init(48, 48);
|
||||||
|
|
||||||
const clear_color = Color.rgb(40, 40, 40);
|
pub fn drawWorld(self: *App, world: *World) !void {
|
||||||
|
inline for (.{ Entity.Layer.ground, Entity.Layer.on_ground, Entity.Layer.surface }) |layer| {
|
||||||
|
var entity_iter = world.entities.iterator();
|
||||||
|
while (entity_iter.next()) |entity_id| {
|
||||||
|
const entity = world.entities.getAssumeExists(entity_id);
|
||||||
|
if (entity.visual_layer != layer) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sprite = entity.sprite;
|
||||||
|
|
||||||
|
const box_hole_margin = 0.1;
|
||||||
|
if (entity.type == .box) {
|
||||||
|
const box_hole_id = world.findEntityId(.{ .type = .box_hole, .pos = entity.pos });
|
||||||
|
if (world.entities.get(box_hole_id)) |box_hole| {
|
||||||
|
if (entity.box_hole_id == box_hole_id and box_hole.visual_pos.distance(entity.visual_pos) < box_hole_margin) {
|
||||||
|
sprite = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (world.findEntity(.{ .type = .box_goal, .pos = entity.pos })) |box_goal| {
|
||||||
|
if (box_goal.visual_pos.distance(entity.visual_pos) < 0.2) {
|
||||||
|
var is_correct = true;
|
||||||
|
if (box_goal.color != .none) {
|
||||||
|
is_correct = box_goal.color == entity.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_correct) {
|
||||||
|
sprite = self.box_sprites_by_color.get(entity.color).darker;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (entity.type == .box_hole) {
|
||||||
|
var box_iter = world.iterEntities(.{ .type = .box, .pos = entity.pos });
|
||||||
|
while (box_iter.next()) |box_id| {
|
||||||
|
const box = world.entities.getAssumeExists(box_id);
|
||||||
|
if (box.visual_pos.distance(entity.visual_pos) < box_hole_margin) {
|
||||||
|
sprite = self.box_sprites_by_color.get(box.color).in_hole;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (entity.type == .player) {
|
||||||
|
const frames = self.player_sprites.getDirection(entity.player_direction);
|
||||||
|
|
||||||
|
sprite = frames[entity.player_frame_index];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sprite != null) {
|
||||||
|
Gfx.drawSprite(
|
||||||
|
sprite.?,
|
||||||
|
entity.visual_pos.multiply(tile_size),
|
||||||
|
tile_size,
|
||||||
|
.white
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn frameGameScene(self: *App, plt: *Platform.Frame) !void {
|
||||||
Gfx.setClearColor(clear_color);
|
Gfx.setClearColor(clear_color);
|
||||||
Gfx.setDefaultSampler(.linear);
|
Gfx.setDefaultSampler(.linear);
|
||||||
|
|
||||||
const tile_size = Vec2.init(48, 48);
|
|
||||||
var canvas = CanvasScaler{
|
|
||||||
.size = Vec2.initFromInt(usize, self.world.width, self.world.height).multiply(tile_size)
|
|
||||||
};
|
|
||||||
canvas.push();
|
|
||||||
defer canvas.pop(clear_color);
|
|
||||||
|
|
||||||
const dt = plt.deltaTime();
|
const dt = plt.deltaTime();
|
||||||
|
|
||||||
|
if (plt.input.isKeyPressed(.ESCAPE)) {
|
||||||
|
self.game_scene.paused = !self.game_scene.paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
var world = &self.game_scene.world;
|
||||||
|
|
||||||
|
if (!self.game_scene.paused) {
|
||||||
|
const input = getPlayerInput(plt.input);
|
||||||
|
|
||||||
if (input.undo) {
|
if (input.undo) {
|
||||||
if (self.undo_stack.pop()) |undo_frame| {
|
if (self.game_scene.undo_stack.pop()) |undo_frame| {
|
||||||
undo_frame.apply(&self.world);
|
undo_frame.apply(world);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.restart) {
|
if (input.restart) {
|
||||||
try self.setWorldByLevel(self.current_level);
|
self.setWorldByLevel(self.game_scene.current_level);
|
||||||
}
|
}
|
||||||
|
|
||||||
var tick_context: TickContext = .{
|
var tick_context: TickContext = .{ .world = world };
|
||||||
.world = &self.world
|
|
||||||
};
|
|
||||||
|
|
||||||
{
|
{
|
||||||
var entity_iter = self.world.entities.iterator();
|
var entity_iter = world.entities.iterator();
|
||||||
while (entity_iter.next()) |entity_id| {
|
while (entity_iter.next()) |entity_id| {
|
||||||
var interpolate_pos = false;
|
var interpolate_pos = false;
|
||||||
|
|
||||||
const entity = self.world.entities.getAssumeExists(entity_id);
|
const entity = world.entities.getAssumeExists(entity_id);
|
||||||
if (entity.type == .player) {
|
if (entity.type == .player) {
|
||||||
interpolate_pos = true;
|
interpolate_pos = true;
|
||||||
|
|
||||||
@ -693,35 +764,34 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
|
|||||||
const frames = self.player_sprites.getDirection(entity.player_direction);
|
const frames = self.player_sprites.getDirection(entity.player_direction);
|
||||||
|
|
||||||
if (target_visual_pos.sub(entity.visual_pos).length() > 0.1) {
|
if (target_visual_pos.sub(entity.visual_pos).length() > 0.1) {
|
||||||
self.animation_timer += plt.input.delta_time;
|
self.game_scene.animation_timer += plt.input.delta_time;
|
||||||
const animatin_time = std.time.ns_per_s / 5;
|
const animatin_time = std.time.ns_per_s / 5;
|
||||||
while (self.animation_timer >= animatin_time) {
|
while (self.game_scene.animation_timer >= animatin_time) {
|
||||||
entity.player_frame_index = (entity.player_frame_index + 1) % @as(u32, @intCast(frames.len));
|
entity.player_frame_index = (entity.player_frame_index + 1) % @as(u32, @intCast(frames.len));
|
||||||
self.animation_timer -= animatin_time;
|
self.game_scene.animation_timer -= animatin_time;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.animation_timer = 0;
|
self.game_scene.animation_timer = 0;
|
||||||
entity.player_frame_index = 0;
|
entity.player_frame_index = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
entity.sprite = frames[entity.player_frame_index];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tick_context.undo_frame.isEmpty()) {
|
if (!tick_context.undo_frame.isEmpty()) {
|
||||||
try self.undo_stack.append(plt.gpa, tick_context.undo_frame);
|
try self.game_scene.undo_stack.append(plt.gpa, tick_context.undo_frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
var goal_count: u32 = 0;
|
var goal_count: u32 = 0;
|
||||||
var box_at_goal_count: u32 = 0;
|
var box_at_goal_count: u32 = 0;
|
||||||
{
|
|
||||||
var goal_iter = self.world.iterEntities(.{ .type = .box_goal });
|
var goal_iter = world.iterEntities(.{ .type = .box_goal });
|
||||||
while (goal_iter.next()) |goal_id| {
|
while (goal_iter.next()) |goal_id| {
|
||||||
const goal = self.world.entities.getAssumeExists(goal_id);
|
const goal = world.entities.getAssumeExists(goal_id);
|
||||||
goal_count += 1;
|
goal_count += 1;
|
||||||
|
|
||||||
if (self.world.findEntity(.{ .pos = goal.pos, .type = .box })) |box| {
|
if (world.findEntity(.{ .pos = goal.pos, .type = .box })) |box| {
|
||||||
var has_correct_color = true;
|
var has_correct_color = true;
|
||||||
if (goal.color != .none) {
|
if (goal.color != .none) {
|
||||||
has_correct_color = goal.color == box.color;
|
has_correct_color = goal.color == box.color;
|
||||||
@ -733,69 +803,83 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
inline for (.{ Entity.Layer.ground, Entity.Layer.on_ground, Entity.Layer.surface }) |layer| {
|
if (goal_count > 0 and goal_count == box_at_goal_count and self.game_scene.current_level+1 < self.levels.items.len) {
|
||||||
var entity_iter = self.world.entities.iterator();
|
self.game_scene.current_level += 1;
|
||||||
while (entity_iter.next()) |entity_id| {
|
self.setWorldByLevel(self.game_scene.current_level);
|
||||||
const entity = self.world.entities.getAssumeExists(entity_id);
|
|
||||||
if (entity.visual_layer != layer) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sprite = entity.sprite;
|
|
||||||
|
|
||||||
const box_hole_margin = 0.1;
|
|
||||||
if (entity.type == .box) {
|
|
||||||
const box_hole_id = self.world.findEntityId(.{ .type = .box_hole, .pos = entity.pos });
|
|
||||||
if (self.world.entities.get(box_hole_id)) |box_hole| {
|
|
||||||
if (entity.box_hole_id == box_hole_id and box_hole.visual_pos.distance(entity.visual_pos) < box_hole_margin) {
|
|
||||||
sprite = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self.world.findEntity(.{ .type = .box_goal, .pos = entity.pos })) |box_goal| {
|
|
||||||
if (box_goal.visual_pos.distance(entity.visual_pos) < 0.2) {
|
|
||||||
var is_correct = true;
|
|
||||||
if (box_goal.color != .none) {
|
|
||||||
is_correct = box_goal.color == entity.color;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_correct) {
|
|
||||||
sprite = self.box_sprites_by_color.get(entity.color).darker;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (entity.type == .box_hole) {
|
|
||||||
var box_iter = self.world.iterEntities(.{ .type = .box, .pos = entity.pos });
|
|
||||||
while (box_iter.next()) |box_id| {
|
|
||||||
const box = self.world.entities.getAssumeExists(box_id);
|
|
||||||
if (box.visual_pos.distance(entity.visual_pos) < box_hole_margin) {
|
|
||||||
sprite = self.box_sprites_by_color.get(box.color).in_hole;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sprite != null) {
|
{
|
||||||
Gfx.drawSprite(
|
var canvas = CanvasScaler{
|
||||||
sprite.?,
|
.size = Vec2.initFromInt(usize, world.width, world.height).multiply(tile_size)
|
||||||
entity.visual_pos.multiply(tile_size),
|
};
|
||||||
tile_size,
|
canvas.push();
|
||||||
.white
|
defer canvas.pop(clear_color);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (goal_count > 0 and goal_count == box_at_goal_count and self.current_level+1 < self.levels.items.len) {
|
try self.drawWorld(&self.game_scene.world);
|
||||||
self.current_level += 1;
|
|
||||||
try self.setWorldByLevel(self.current_level);
|
if (self.game_scene.paused) {
|
||||||
|
var color = clear_color;
|
||||||
|
color.a = 0.8;
|
||||||
|
Gfx.drawRectangle(.init(0, 0), canvas.size, color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setWorldByLevel(self: *App, index: u32) !void {
|
pub fn frameMainMenu(self: *App, plt: *Platform.Frame) !void {
|
||||||
self.world.copy(self.levels.items[index]);
|
Gfx.setClearColor(clear_color);
|
||||||
self.undo_stack.clearRetainingCapacity();
|
Gfx.setDefaultSampler(.linear);
|
||||||
|
|
||||||
|
var canvas = CanvasScaler{
|
||||||
|
.size = Vec2.initFromInt(usize, 1920, 1080)
|
||||||
|
};
|
||||||
|
canvas.push();
|
||||||
|
defer canvas.pop(clear_color);
|
||||||
|
|
||||||
|
var ui = UI.init(&self.main_menu_state, plt.input);
|
||||||
|
ui.beginFrame();
|
||||||
|
defer ui.endFrame();
|
||||||
|
|
||||||
|
const ui_frame = ui.top();
|
||||||
|
const font = &self.roboto_font;
|
||||||
|
ui_frame.font = font;
|
||||||
|
ui_frame.text_color = .black;
|
||||||
|
ui_frame.text_height = 80;
|
||||||
|
ui_frame.container = Rect{ .pos = .init(0, 0), .size = canvas.size };
|
||||||
|
ui_frame.container = ui_frame.container.shrinkPercent(0.1);
|
||||||
|
|
||||||
|
ui_frame.padding.addHorizontal(100);
|
||||||
|
ui_frame.padding.addVertical(50);
|
||||||
|
|
||||||
|
var layout = UI.VerticalLayout{ .ui = &ui, .gap = 30 };
|
||||||
|
|
||||||
|
if (layout.textButton("Play")) {
|
||||||
|
log.debug("press play", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layout.textButton("Settings")) {
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!builtin.target.cpu.arch.isWasm()) {
|
||||||
|
if (layout.textButton("Exit")) {
|
||||||
|
plt.request_quit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn frame(self: *App, plt: *Platform.Frame) !void {
|
||||||
|
if (self.in_main_menu) {
|
||||||
|
try self.frameMainMenu(plt);
|
||||||
|
} else {
|
||||||
|
try self.frameGameScene(plt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setWorldByLevel(self: *App, index: u32) void {
|
||||||
|
self.game_scene.world.copy(self.levels.items[index]);
|
||||||
|
self.game_scene.undo_stack.clearRetainingCapacity();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *App, plt: Platform.Deinit) void {
|
pub fn deinit(self: *App, plt: Platform.Deinit) void {
|
||||||
@ -803,5 +887,5 @@ pub fn deinit(self: *App, plt: Platform.Deinit) void {
|
|||||||
Gfx.deinitSpritesheet(self.roboto_font.spritesheet);
|
Gfx.deinitSpritesheet(self.roboto_font.spritesheet);
|
||||||
|
|
||||||
self.levels.deinit(plt.gpa);
|
self.levels.deinit(plt.gpa);
|
||||||
self.undo_stack.deinit(plt.gpa);
|
self.game_scene.undo_stack.deinit(plt.gpa);
|
||||||
}
|
}
|
||||||
|
|||||||
50
src/font.zig
50
src/font.zig
@ -93,19 +93,20 @@ pub fn getGlyph(font: *Font, index: Glyph.Index, scale_x: f32, scale_y: f32) ?*F
|
|||||||
|
|
||||||
var bitmap_buffer: [Font.Glyph.max_width * Font.Glyph.max_height]u8 = undefined;
|
var bitmap_buffer: [Font.Glyph.max_width * Font.Glyph.max_height]u8 = undefined;
|
||||||
const bitmap = Gfx.Image{
|
const bitmap = Gfx.Image{
|
||||||
.pixels = .{ .r8 = (&bitmap_buffer).ptr },
|
.pixels = (&bitmap_buffer).ptr,
|
||||||
|
.format = .r8,
|
||||||
.width = box_width,
|
.width = box_width,
|
||||||
.height = box_height
|
.height = box_height
|
||||||
};
|
};
|
||||||
font.stb.makeGlyphBitmap(
|
font.stb.makeGlyphBitmap(
|
||||||
bitmap.pixels.?.r8,
|
bitmap.pixels,
|
||||||
box_width, box_height,
|
box_width, box_height,
|
||||||
box_width,
|
box_width,
|
||||||
glyph_key.scale_x.toF32(),
|
glyph_key.scale_x.toF32(),
|
||||||
glyph_key.scale_y.toF32(),
|
glyph_key.scale_y.toF32(),
|
||||||
glyph_key.index
|
glyph_key.index
|
||||||
);
|
);
|
||||||
Gfx.setSprite(sprite, .{ .image = bitmap });
|
Gfx.setSprite(sprite, .{ .mipchain = .initSingle(bitmap) });
|
||||||
|
|
||||||
glyph.sprite = sprite;
|
glyph.sprite = sprite;
|
||||||
}
|
}
|
||||||
@ -125,9 +126,17 @@ pub fn getAscent(font: *Font, height: f32) f32 {
|
|||||||
return @as(f32, @floatFromInt(vmetrics.ascent)) * scale;
|
return @as(f32, @floatFromInt(vmetrics.ascent)) * scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn measureText(font: *Font, height: f32, text: []const u8) Rect {
|
pub fn getAscentDescent(font: *Font, height: f32) f32 {
|
||||||
|
const vmetrics = font.stb.getFontVMetrics();
|
||||||
|
const scale = font.stb.scaleForPixelHeight(height);
|
||||||
|
return @as(f32, @floatFromInt(vmetrics.ascent - vmetrics.descent)) * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn measureText(font: *Font, text: []const u8, height: f32) Rect {
|
||||||
var bounds: Rect = .init(0, 0, 0, 0);
|
var bounds: Rect = .init(0, 0, 0, 0);
|
||||||
var first_glyph = false;
|
var first_glyph = true;
|
||||||
|
|
||||||
|
const ascent = font.getAscent(height);
|
||||||
|
|
||||||
var layout = TextRunLayout.init(font, height);
|
var layout = TextRunLayout.init(font, height);
|
||||||
var iter = std.unicode.Utf8Iterator{
|
var iter = std.unicode.Utf8Iterator{
|
||||||
@ -138,11 +147,14 @@ pub fn measureText(font: *Font, height: f32, text: []const u8) Rect {
|
|||||||
const glyph_index = font.getGlyphIndex(codepoint) orelse continue;
|
const glyph_index = font.getGlyphIndex(codepoint) orelse continue;
|
||||||
const glyph_layout = layout.next(glyph_index) orelse continue;
|
const glyph_layout = layout.next(glyph_index) orelse continue;
|
||||||
|
|
||||||
|
var glyph_rect = glyph_layout.rect;
|
||||||
|
glyph_rect.pos.y += ascent;
|
||||||
|
|
||||||
if (first_glyph) {
|
if (first_glyph) {
|
||||||
bounds = glyph_layout.rect;
|
bounds = glyph_rect;
|
||||||
first_glyph = false;
|
first_glyph = false;
|
||||||
} else {
|
} else {
|
||||||
bounds = bounds.expand(glyph_layout.rect);
|
bounds = bounds.expand(glyph_rect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,20 +165,13 @@ pub const DrawTextOptions = struct {
|
|||||||
text: []const u8,
|
text: []const u8,
|
||||||
pos: Vec2,
|
pos: Vec2,
|
||||||
height: f32,
|
height: f32,
|
||||||
color: Gfx.Color = .white,
|
color: Gfx.Color,
|
||||||
alignment: Vec2 = .init(0, 0)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn drawText(font: *Font, opts: DrawTextOptions) void {
|
pub fn drawText(font: *Font, opts: DrawTextOptions) void {
|
||||||
var layout = Font.TextRunLayout.init(font, opts.height);
|
var layout = Font.TextRunLayout.init(font, opts.height);
|
||||||
|
|
||||||
var pos = opts.pos;
|
const ascent = font.getAscent(opts.height);
|
||||||
pos.y += font.getAscent(opts.height);
|
|
||||||
|
|
||||||
if (!opts.alignment.eql(.zero)) {
|
|
||||||
const bounds = font.measureText(opts.height, opts.text);
|
|
||||||
pos = pos.sub(bounds.size.multiply(opts.alignment));
|
|
||||||
}
|
|
||||||
|
|
||||||
var iter = std.unicode.Utf8Iterator{
|
var iter = std.unicode.Utf8Iterator{
|
||||||
.bytes = opts.text,
|
.bytes = opts.text,
|
||||||
@ -179,7 +184,8 @@ pub fn drawText(font: *Font, opts: DrawTextOptions) void {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var glyph_pos = glyph_layout.rect.pos.add(pos);
|
var glyph_pos = glyph_layout.rect.pos.add(opts.pos);
|
||||||
|
glyph_pos.y += ascent;
|
||||||
|
|
||||||
// Round to nearest pixel to avoid artifacts
|
// Round to nearest pixel to avoid artifacts
|
||||||
glyph_pos.x = @round(glyph_pos.x);
|
glyph_pos.x = @round(glyph_pos.x);
|
||||||
@ -316,9 +322,13 @@ const GlyphCache = struct {
|
|||||||
found_existing: bool,
|
found_existing: bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn keyToHashMapIndex(self: *GlyphCache, key: Glyph.Key, offset: usize) usize {
|
||||||
|
return @intCast((key.toInt() + offset) % self.glyphs_hashmap.len);
|
||||||
|
}
|
||||||
|
|
||||||
fn insertOrGet(self: *GlyphCache, key: Glyph.Key) ?InsertOrGetResult {
|
fn insertOrGet(self: *GlyphCache, key: Glyph.Key) ?InsertOrGetResult {
|
||||||
for (0..self.glyphs_hashmap.len) |offset| {
|
for (0..self.glyphs_hashmap.len) |offset| {
|
||||||
const i = (key.toInt() + offset) % self.glyphs_hashmap.len;
|
const i = self.keyToHashMapIndex(key, offset);
|
||||||
const hashmap_id = self.glyphs_hashmap[i];
|
const hashmap_id = self.glyphs_hashmap[i];
|
||||||
if (hashmap_id == GlyphSlot.nil_id) {
|
if (hashmap_id == GlyphSlot.nil_id) {
|
||||||
const id = self.popFreeId() orelse return null;
|
const id = self.popFreeId() orelse return null;
|
||||||
@ -350,7 +360,7 @@ const GlyphCache = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn removeFromHashmap(self: *GlyphCache, id: GlyphSlot.Id) void {
|
fn removeFromHashmap(self: *GlyphCache, id: GlyphSlot.Id) void {
|
||||||
var bucket = self.glyphs[id].glyph.key.toInt() % self.glyphs_hashmap.len;
|
var bucket = self.keyToHashMapIndex(self.glyphs[id].glyph.key, 0);
|
||||||
for (0..self.glyphs_hashmap.len) |_| {
|
for (0..self.glyphs_hashmap.len) |_| {
|
||||||
assert(self.glyphs_hashmap[bucket] != GlyphSlot.nil_id);
|
assert(self.glyphs_hashmap[bucket] != GlyphSlot.nil_id);
|
||||||
if (self.glyphs_hashmap[bucket] == id) {
|
if (self.glyphs_hashmap[bucket] == id) {
|
||||||
@ -367,7 +377,7 @@ const GlyphCache = struct {
|
|||||||
|
|
||||||
while (self.glyphs_hashmap[current] != GlyphSlot.nil_id) {
|
while (self.glyphs_hashmap[current] != GlyphSlot.nil_id) {
|
||||||
const current_glyph_id = self.glyphs_hashmap[current];
|
const current_glyph_id = self.glyphs_hashmap[current];
|
||||||
const home = self.glyphs[current_glyph_id].glyph.key.toInt() % self.glyphs_hashmap.len;
|
const home = self.keyToHashMapIndex(self.glyphs[current_glyph_id].glyph.key, 0);
|
||||||
|
|
||||||
if (self.hashmapDistance(home, current) > self.hashmapDistance(home, empty)) {
|
if (self.hashmapDistance(home, current) > self.hashmapDistance(home, empty)) {
|
||||||
self.glyphs_hashmap[empty] = self.glyphs_hashmap[current];
|
self.glyphs_hashmap[empty] = self.glyphs_hashmap[current];
|
||||||
|
|||||||
@ -22,6 +22,7 @@ const SlotMapType = @import("slot_map.zig").SlotMapType;
|
|||||||
|
|
||||||
const Frame = struct {
|
const Frame = struct {
|
||||||
index: u32,
|
index: u32,
|
||||||
|
draw_calls: u32 = 0,
|
||||||
quads: std.ArrayList(VertexQuad),
|
quads: std.ArrayList(VertexQuad),
|
||||||
transforms: std.ArrayList(TransformFrame),
|
transforms: std.ArrayList(TransformFrame),
|
||||||
|
|
||||||
@ -494,6 +495,8 @@ pub fn flush() void {
|
|||||||
sg.applyUniforms(shd.UB_fs_params, sg.asRange(&fs_params));
|
sg.applyUniforms(shd.UB_fs_params, sg.asRange(&fs_params));
|
||||||
assert(self.frame.quads.items.len > 0);
|
assert(self.frame.quads.items.len > 0);
|
||||||
sg.draw(0, @intCast(self.frame.quads.items.len * 6), 1);
|
sg.draw(0, @intCast(self.frame.quads.items.len * 6), 1);
|
||||||
|
|
||||||
|
self.frame.draw_calls += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn endFrame() void {
|
pub fn endFrame() void {
|
||||||
@ -501,6 +504,13 @@ pub fn endFrame() void {
|
|||||||
|
|
||||||
flush();
|
flush();
|
||||||
|
|
||||||
|
// TODO: Workaround for when nothing is drawn.
|
||||||
|
// Just push anything so that screen is cleared
|
||||||
|
if (self.frame.draw_calls == 0) {
|
||||||
|
drawRectangle(.init(0, 0), .init(0, 0), .black);
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
|
||||||
if (self.frame.pass_started) {
|
if (self.frame.pass_started) {
|
||||||
sg.endPass();
|
sg.endPass();
|
||||||
sg.commit();
|
sg.commit();
|
||||||
@ -1435,6 +1445,7 @@ pub const Image = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn extrudeEdge(self: Image, rect: Image.Rect, size: u32) void {
|
pub fn extrudeEdge(self: Image, rect: Image.Rect, size: u32) void {
|
||||||
|
// Left edge
|
||||||
for (0..rect.width) |ox| {
|
for (0..rect.width) |ox| {
|
||||||
const pixel = self.getPixel(rect.x+ox, rect.y);
|
const pixel = self.getPixel(rect.x+ox, rect.y);
|
||||||
for (0..size) |i| {
|
for (0..size) |i| {
|
||||||
@ -1442,6 +1453,7 @@ pub const Image = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Right edge
|
||||||
for (0..rect.width) |ox| {
|
for (0..rect.width) |ox| {
|
||||||
const pixel = self.getPixel(rect.x+ox, rect.y+rect.height-1);
|
const pixel = self.getPixel(rect.x+ox, rect.y+rect.height-1);
|
||||||
for (0..size) |i| {
|
for (0..size) |i| {
|
||||||
@ -1449,6 +1461,7 @@ pub const Image = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Top edge
|
||||||
for (0..rect.height) |oy| {
|
for (0..rect.height) |oy| {
|
||||||
const pixel = self.getPixel(rect.x, rect.y+oy);
|
const pixel = self.getPixel(rect.x, rect.y+oy);
|
||||||
for (0..size) |i| {
|
for (0..size) |i| {
|
||||||
@ -1456,6 +1469,7 @@ pub const Image = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bottom edge
|
||||||
for (0..rect.height) |oy| {
|
for (0..rect.height) |oy| {
|
||||||
const pixel = self.getPixel(rect.x+rect.width-1, rect.y+oy);
|
const pixel = self.getPixel(rect.x+rect.width-1, rect.y+oy);
|
||||||
for (0..size) |i| {
|
for (0..size) |i| {
|
||||||
@ -1463,7 +1477,29 @@ pub const Image = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add padding to corners.
|
// Top left
|
||||||
|
{
|
||||||
|
const pixel = self.getPixel(rect.x, rect.y);
|
||||||
|
self.setRect(rect.x-size, rect.y-size, size, size, pixel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top right
|
||||||
|
{
|
||||||
|
const pixel = self.getPixel(rect.x+rect.width-1, rect.y);
|
||||||
|
self.setRect(rect.x+rect.width, rect.y-size, size, size, pixel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom left
|
||||||
|
{
|
||||||
|
const pixel = self.getPixel(rect.x, rect.y+rect.height-1);
|
||||||
|
self.setRect(rect.x-size, rect.y+rect.height, size, size, pixel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom right
|
||||||
|
{
|
||||||
|
const pixel = self.getPixel(rect.x+rect.width-1, rect.y+rect.height-1);
|
||||||
|
self.setRect(rect.x+rect.width, rect.y+rect.height, size, size, pixel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1752,4 +1788,8 @@ pub const TransformFrame = struct {
|
|||||||
pub fn apply(self: TransformFrame, position: Vec2) Vec2 {
|
pub fn apply(self: TransformFrame, position: Vec2) Vec2 {
|
||||||
return position.multiply(self.scale).add(self.offset);
|
return position.multiply(self.scale).add(self.offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn applyInverse(self: TransformFrame, position: Vec2) Vec2 {
|
||||||
|
return position.sub(self.offset).divide(self.scale);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -464,6 +464,10 @@ pub const Rect = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn shrinkPercent(self: Rect, percent: f32) Rect {
|
||||||
|
return self.shrink(self.size.multiplyScalar(percent / 2));
|
||||||
|
}
|
||||||
|
|
||||||
pub fn grow(self: Rect, margin: Vec2) Rect {
|
pub fn grow(self: Rect, margin: Vec2) Rect {
|
||||||
return self.shrink(margin.multiplyScalar(-1));
|
return self.shrink(margin.multiplyScalar(-1));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -126,16 +126,21 @@ fn PlatformType(App: type) type {
|
|||||||
ImGUI.beginFrame(self.show_imgui, self.frame_arena.allocator());
|
ImGUI.beginFrame(self.show_imgui, self.frame_arena.allocator());
|
||||||
|
|
||||||
if (@hasDecl(App, "frame")) {
|
if (@hasDecl(App, "frame")) {
|
||||||
const plt = Frame{
|
var plt = Frame{
|
||||||
.gpa = self.gpa,
|
.gpa = self.gpa,
|
||||||
.arena = self.arena.allocator(),
|
.arena = self.arena.allocator(),
|
||||||
.io = self.io,
|
.io = self.io,
|
||||||
.input = &self.input,
|
.input = &self.input,
|
||||||
.input_events = self.events.items,
|
.input_events = self.events.items,
|
||||||
.frame = self.frame_arena.allocator(),
|
.frame = self.frame_arena.allocator(),
|
||||||
.assets = &self.assets
|
.assets = &self.assets,
|
||||||
|
.request_quit = false
|
||||||
};
|
};
|
||||||
try self.app.frame(plt);
|
try self.app.frame(&plt);
|
||||||
|
|
||||||
|
if (plt.request_quit) {
|
||||||
|
sapp.requestQuit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Gfx.flush();
|
Gfx.flush();
|
||||||
@ -603,6 +608,7 @@ pub const Frame = struct {
|
|||||||
assets: *Assets,
|
assets: *Assets,
|
||||||
input: *Input,
|
input: *Input,
|
||||||
input_events: []Input.Event,
|
input_events: []Input.Event,
|
||||||
|
request_quit: bool,
|
||||||
|
|
||||||
fn nanosecondsToSeconds(nanoseconds: Nanoseconds) f32 {
|
fn nanosecondsToSeconds(nanoseconds: Nanoseconds) f32 {
|
||||||
return @floatCast(@as(f64, @floatFromInt(nanoseconds)) / std.time.ns_per_s);
|
return @floatCast(@as(f64, @floatFromInt(nanoseconds)) / std.time.ns_per_s);
|
||||||
|
|||||||
335
src/ui.zig
Normal file
335
src/ui.zig
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const log = std.log;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const Font = @import("./font.zig");
|
||||||
|
|
||||||
|
const Platform = @import("platform");
|
||||||
|
const Gfx = Platform.Gfx;
|
||||||
|
const Math = Platform.Math;
|
||||||
|
const Color = Platform.Color;
|
||||||
|
const Vec2 = Platform.Vec2;
|
||||||
|
const Rect = Math.Rect;
|
||||||
|
|
||||||
|
const UI = @This();
|
||||||
|
|
||||||
|
pub const Key = enum(u64) {
|
||||||
|
_,
|
||||||
|
|
||||||
|
const StringHasher = std.hash.Wyhash;
|
||||||
|
pub const CombineHasher = std.hash.Fnv1a_64;
|
||||||
|
|
||||||
|
pub const nil = Key.init(0);
|
||||||
|
|
||||||
|
pub fn init(hash: u64) Key {
|
||||||
|
return @enumFromInt(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initPtr(ptr: anytype) Key {
|
||||||
|
return Key.initUsize(@intFromPtr(ptr));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initUsize(num: usize) Key {
|
||||||
|
return Key.init(@truncate(num));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initString(seed: u64, text: []const u8) Key {
|
||||||
|
return Key.init(StringHasher.hash(seed, text));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn combine(self: Key, other: Key) Key {
|
||||||
|
var hasher = CombineHasher.init();
|
||||||
|
hasher.update(std.mem.asBytes(&@intFromEnum(self)));
|
||||||
|
hasher.update(std.mem.asBytes(&@intFromEnum(other)));
|
||||||
|
|
||||||
|
return Key.init(hasher.final());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isNil(self: Key) bool {
|
||||||
|
return self == nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format(self: Key, writer: *std.io.Writer) std.io.Writer.Error!void {
|
||||||
|
try writer.print("{s}{{ 0x{x:016} }}", .{ @typeName(Key), @intFromEnum(self) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const VerticalLayout = struct {
|
||||||
|
ui: *UI,
|
||||||
|
used: f32 = 0,
|
||||||
|
gap: f32 = 0,
|
||||||
|
|
||||||
|
pub fn advance(self: *VerticalLayout, height: f32) void {
|
||||||
|
self.used += height;
|
||||||
|
self.used += self.gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(self: *VerticalLayout, height: f32) Rect {
|
||||||
|
const container = self.ui.top().container;
|
||||||
|
const result = Rect{
|
||||||
|
.pos = container.pos.add(.init(0, self.used)),
|
||||||
|
.size = .init(container.size.x, height)
|
||||||
|
};
|
||||||
|
|
||||||
|
self.advance(height);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn textButton(self: *VerticalLayout, text: []const u8) bool {
|
||||||
|
const frame = self.ui.top();
|
||||||
|
const font = frame.font orelse return false;
|
||||||
|
|
||||||
|
var button_size = font.measureText(text, frame.text_height).size;
|
||||||
|
button_size = button_size.add(frame.padding.toVec2());
|
||||||
|
|
||||||
|
const row_rect = self.next(button_size.y);
|
||||||
|
const button_rect = Rect{
|
||||||
|
.pos = row_rect.pos,
|
||||||
|
.size = button_size
|
||||||
|
};
|
||||||
|
|
||||||
|
return self.ui.textButton(button_rect, text);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Sides = struct {
|
||||||
|
left: f32,
|
||||||
|
right: f32,
|
||||||
|
top: f32,
|
||||||
|
bottom: f32,
|
||||||
|
|
||||||
|
pub const empty = Sides{
|
||||||
|
.left = 0,
|
||||||
|
.right = 0,
|
||||||
|
.top = 0,
|
||||||
|
.bottom = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn grow(self: Sides, rect: Rect) Rect {
|
||||||
|
return Rect{
|
||||||
|
.pos = rect.pos.sub(.init(self.left, self.top)),
|
||||||
|
.size = rect.size.add(.init(self.left+self.right, self.top+self.bottom))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shrink(self: Sides, rect: Rect) Rect {
|
||||||
|
return Rect{
|
||||||
|
.pos = rect.pos.add(.init(self.left, self.top)),
|
||||||
|
.size = rect.size.sub(.init(self.left+self.right, self.top+self.bottom))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(lhs: Sides, rhs: Sides) Sides {
|
||||||
|
return Sides{
|
||||||
|
.left = lhs.left + rhs.left,
|
||||||
|
.right = lhs.right + rhs.right,
|
||||||
|
.top = lhs.top + rhs.top,
|
||||||
|
.bottom = lhs.bottom + rhs.bottom,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initVertical(amount: f32) Sides {
|
||||||
|
return Sides{
|
||||||
|
.top = amount/2,
|
||||||
|
.bottom = amount/2,
|
||||||
|
.left = 0,
|
||||||
|
.right = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initHorizontal(amount: f32) Sides {
|
||||||
|
return Sides{
|
||||||
|
.left = amount/2,
|
||||||
|
.right = amount/2,
|
||||||
|
.top = 0,
|
||||||
|
.bottom = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn vertical(self: Sides) f32 {
|
||||||
|
return self.top + self.bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn horizontal(self: Sides) f32 {
|
||||||
|
return self.left + self.right;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn addVertical(self: *Sides, amount: f32) void {
|
||||||
|
self.* = self.add(.initVertical(amount));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn addHorizontal(self: *Sides, amount: f32) void {
|
||||||
|
self.* = self.add(.initHorizontal(amount));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toVec2(self: Sides) Vec2 {
|
||||||
|
return Vec2.init(self.horizontal(), self.vertical());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Box = struct {
|
||||||
|
margin: Sides,
|
||||||
|
padding: Sides,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Frame = struct {
|
||||||
|
container: Rect = .zero,
|
||||||
|
key: Key = .nil,
|
||||||
|
font: ?*Font = null,
|
||||||
|
text_color: Color = .white,
|
||||||
|
text_height: f32 = 64,
|
||||||
|
margin: Sides = .empty,
|
||||||
|
padding: Sides = .empty,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const State = struct {
|
||||||
|
hot: Key,
|
||||||
|
active: Key,
|
||||||
|
|
||||||
|
pub const empty = State{
|
||||||
|
.hot = .nil,
|
||||||
|
.active = .nil
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
state: *State,
|
||||||
|
input: *Platform.Input,
|
||||||
|
stack: [32]Frame,
|
||||||
|
stack_len: u32,
|
||||||
|
|
||||||
|
pub fn init(state: *State, input: *Platform.Input) UI {
|
||||||
|
var stack: [32]Frame = undefined;
|
||||||
|
stack[0] = Frame{};
|
||||||
|
|
||||||
|
return UI{
|
||||||
|
.state = state,
|
||||||
|
.input = input,
|
||||||
|
.stack = stack,
|
||||||
|
.stack_len = 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn beginFrame(self: *UI) void {
|
||||||
|
if (self.state.active == Key.nil) {
|
||||||
|
self.state.hot = .nil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn endFrame(self: *UI) void {
|
||||||
|
if (self.input.isMouseReleased(.left)) {
|
||||||
|
self.state.active = .nil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(self: *UI) void {
|
||||||
|
if (self.stack_len == self.stack.len) {
|
||||||
|
log.warn("UI stack capacity limit reached", .{});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assert(self.stack_len >= 1);
|
||||||
|
self.stack[self.stack_len] = self.stack[self.stack_len-1];
|
||||||
|
self.stack_len += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pop(self: *UI) void {
|
||||||
|
if (self.stack_len <= 1) {
|
||||||
|
log.warn("Attempt to pop from empty UI", .{});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.stack_len -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn top(self: *UI) *Frame {
|
||||||
|
assert(self.stack_len > 0);
|
||||||
|
return &self.stack[self.stack_len-1];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn measureText(self: *UI, text: []const u8) Rect {
|
||||||
|
const frame = self.top();
|
||||||
|
const font = frame.font orelse return .zero;
|
||||||
|
|
||||||
|
return font.measureText(
|
||||||
|
text,
|
||||||
|
frame.text_height,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn drawText(self: *UI, pos: Vec2, text: []const u8) void {
|
||||||
|
const frame = self.top();
|
||||||
|
const font = frame.font orelse return;
|
||||||
|
|
||||||
|
font.drawText(.{
|
||||||
|
.pos = pos,
|
||||||
|
.text = text,
|
||||||
|
.height = frame.text_height,
|
||||||
|
.color = frame.text_color
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getMousePosition(self: *UI) ?Vec2 {
|
||||||
|
if (self.input.mouse_position) |screen_mouse_position| {
|
||||||
|
return Gfx.getTransform().applyInverse(screen_mouse_position);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ButtonState = enum {
|
||||||
|
normal,
|
||||||
|
hot,
|
||||||
|
active,
|
||||||
|
released,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn buttonLogic(self: *UI, rect: Rect, key: Key) ButtonState {
|
||||||
|
if (self.getMousePosition()) |mouse_position| {
|
||||||
|
const is_inside = rect.isInside(mouse_position);
|
||||||
|
if (self.state.hot == Key.nil and is_inside) {
|
||||||
|
self.state.hot = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.state.hot == key and self.input.isMousePressed(.left)) {
|
||||||
|
self.state.active = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.state.active == key and self.input.isMouseReleased(.left) and is_inside) {
|
||||||
|
return .released;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (self.state.active == key) {
|
||||||
|
return .active;
|
||||||
|
} else if (self.state.hot == key) {
|
||||||
|
return .hot;
|
||||||
|
} else {
|
||||||
|
return .normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn textButton(self: *UI, rect: Rect, text: []const u8) bool {
|
||||||
|
const frame = self.top();
|
||||||
|
const text_bounds = self.measureText(text);
|
||||||
|
|
||||||
|
const button_state = self.buttonLogic(rect, Key.initString(@intFromEnum(frame.key), text));
|
||||||
|
|
||||||
|
if (button_state == .active) {
|
||||||
|
Gfx.drawRectangle(rect.pos, rect.size, .blue);
|
||||||
|
} else if (button_state == .hot) {
|
||||||
|
Gfx.drawRectangle(rect.pos, rect.size, .red);
|
||||||
|
} else {
|
||||||
|
Gfx.drawRectangle(rect.pos, rect.size, .white);
|
||||||
|
}
|
||||||
|
|
||||||
|
var content_size = rect.size.sub(frame.padding.toVec2());
|
||||||
|
content_size.x = @max(content_size.x, 0);
|
||||||
|
content_size.y = @max(content_size.y, 0);
|
||||||
|
|
||||||
|
self.drawText(
|
||||||
|
rect.pos
|
||||||
|
.sub(text_bounds.pos)
|
||||||
|
.add(.init(frame.padding.left, frame.padding.top))
|
||||||
|
.add(content_size.sub(text_bounds.size).multiplyScalar(0.5)),
|
||||||
|
text
|
||||||
|
);
|
||||||
|
|
||||||
|
return button_state == .released;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user