refactor ui
This commit is contained in:
parent
160b7778ce
commit
43b6ca0ff2
615
src/app.zig
615
src/app.zig
@ -1,22 +1,17 @@
|
||||
const std = @import("std");
|
||||
const rl = @import("raylib");
|
||||
const TaskPool = @import("./task-pool.zig");
|
||||
const FontFace = @import("./font-face.zig");
|
||||
const Graph = @import("./ui/graph.zig");
|
||||
const srcery = @import("./srcery.zig");
|
||||
const UI = @import("./ui.zig");
|
||||
const Platform = @import("./platform.zig");
|
||||
const Assets = @import("./assets.zig");
|
||||
const Graph = @import("./graph.zig");
|
||||
const NIDaq = @import("ni-daq.zig");
|
||||
const Theme = @import("./theme.zig");
|
||||
const RectUtils = @import("./rect-utils.zig");
|
||||
const UI = @import("./ui/root.zig");
|
||||
const showButton = @import("./ui/button.zig").showButton;
|
||||
const rect_utils = @import("./rect-utils.zig");
|
||||
const remap = @import("./utils.zig").remap;
|
||||
const Aseprite = @import("./aseprite.zig");
|
||||
|
||||
const log = std.log.scoped(.app);
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
const Vec2 = rl.Vector2;
|
||||
const clamp = std.math.clamp;
|
||||
|
||||
const App = @This();
|
||||
|
||||
@ -24,7 +19,7 @@ const Channel = struct {
|
||||
view_cache: Graph.Cache = .{},
|
||||
view_rect: Graph.ViewOptions,
|
||||
|
||||
dragged_marker: ?enum { from, to, both } = null,
|
||||
height: f32 = 150,
|
||||
|
||||
min_value: f64,
|
||||
max_value: f64,
|
||||
@ -33,83 +28,73 @@ const Channel = struct {
|
||||
},
|
||||
};
|
||||
|
||||
allocator: Allocator,
|
||||
channels: std.ArrayList(Channel),
|
||||
|
||||
channel_samples: ?*TaskPool.ChannelSamples = null,
|
||||
task_pool: TaskPool,
|
||||
|
||||
ni_daq: NIDaq,
|
||||
|
||||
start_time_ns: i128,
|
||||
|
||||
view_from: f32 = 0,
|
||||
view_width: f32 = 5000,
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
ui: UI,
|
||||
channels: std.BoundedArray(Channel, 64) = .{},
|
||||
ni_daq: NIDaq,
|
||||
|
||||
grab_texture: struct {
|
||||
normal: rl.Texture2D,
|
||||
hot: rl.Texture2D,
|
||||
active: rl.Texture2D,
|
||||
},
|
||||
|
||||
pub fn init(
|
||||
allocator: Allocator,
|
||||
task_pool_options: TaskPool.Options,
|
||||
nidaq_options: NIDaq.Options
|
||||
) !App {
|
||||
|
||||
// TODO: Maybe store a compressed version of aseprite files when embedding?
|
||||
// Setup a build step to compress the files
|
||||
const grab_ase = try Aseprite.init(allocator, @embedFile("./assets/grab-marker.ase"));
|
||||
defer grab_ase.deinit();
|
||||
|
||||
const grab_normal_image = grab_ase.getTagImage(grab_ase.getTag("normal") orelse return error.TagNotFound);
|
||||
defer rl.unloadImage(grab_normal_image);
|
||||
const grab_normal_texture = rl.loadTextureFromImage(grab_normal_image);
|
||||
|
||||
const grab_hot_image = grab_ase.getTagImage(grab_ase.getTag("hot") orelse return error.TagNotFound);
|
||||
defer rl.unloadImage(grab_hot_image);
|
||||
const grab_hot_texture = rl.loadTextureFromImage(grab_hot_image);
|
||||
|
||||
const grab_active_image = grab_ase.getTagImage(grab_ase.getTag("active") orelse return error.TagNotFound);
|
||||
defer rl.unloadImage(grab_active_image);
|
||||
const grab_active_texture = rl.loadTextureFromImage(grab_active_image);
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) !App {
|
||||
return App{
|
||||
.allocator = allocator,
|
||||
.task_pool = try TaskPool.init(allocator, task_pool_options),
|
||||
.channels = std.ArrayList(Channel).init(allocator),
|
||||
.ni_daq = try NIDaq.init(allocator, nidaq_options),
|
||||
.start_time_ns = std.time.nanoTimestamp(),
|
||||
.ui = UI.init(),
|
||||
|
||||
.grab_texture = .{
|
||||
.normal = grab_normal_texture,
|
||||
.hot = grab_hot_texture,
|
||||
.active = grab_active_texture
|
||||
}
|
||||
.ui = UI.init(allocator),
|
||||
.ni_daq = try NIDaq.init(allocator, .{
|
||||
.max_devices = 4,
|
||||
.max_analog_inputs = 32,
|
||||
.max_analog_outputs = 8,
|
||||
.max_counter_outputs = 8,
|
||||
.max_counter_inputs = 8,
|
||||
.max_analog_input_voltage_ranges = 4,
|
||||
.max_analog_output_voltage_ranges = 4
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App) void {
|
||||
for (self.channels.items) |*channel| {
|
||||
if (channel.samples == .owned) {
|
||||
self.allocator.free(channel.samples.owned);
|
||||
self.ni_daq.deinit(self.allocator);
|
||||
for (self.channels.slice()) |*channel| {
|
||||
switch (channel.samples) {
|
||||
.owned => |owned| self.allocator.free(owned)
|
||||
}
|
||||
channel.view_cache.deinit();
|
||||
}
|
||||
self.channels.deinit();
|
||||
self.task_pool.deinit(self.allocator);
|
||||
self.ni_daq.deinit(self.allocator);
|
||||
|
||||
rl.unloadTexture(self.grab_texture.normal);
|
||||
rl.unloadTexture(self.grab_texture.hot);
|
||||
rl.unloadTexture(self.grab_texture.active);
|
||||
self.ui.deinit();
|
||||
}
|
||||
|
||||
fn readSamplesFromFile(allocator: Allocator, file: std.fs.File) ![]f64 {
|
||||
fn showButton(self: *App, text: []const u8) UI.Interaction {
|
||||
var button = self.ui.newWidget(self.ui.keyFromString(text));
|
||||
button.border = srcery.bright_blue;
|
||||
button.padding.vertical(8);
|
||||
button.padding.horizontal(16);
|
||||
button.flags.insert(.clickable);
|
||||
button.size = .{
|
||||
.x = .{ .text = {} },
|
||||
.y = .{ .text = {} },
|
||||
};
|
||||
|
||||
const interaction = self.ui.getInteraction(button);
|
||||
var text_color: rl.Color = undefined;
|
||||
if (interaction.held_down) {
|
||||
button.background = srcery.hard_black;
|
||||
text_color = srcery.white;
|
||||
} else if (interaction.hovering) {
|
||||
button.background = srcery.bright_black;
|
||||
text_color = srcery.bright_white;
|
||||
} else {
|
||||
button.background = srcery.blue;
|
||||
text_color = srcery.bright_white;
|
||||
}
|
||||
|
||||
button.text = .{
|
||||
.content = text,
|
||||
.color = text_color
|
||||
};
|
||||
|
||||
return interaction;
|
||||
}
|
||||
|
||||
fn readSamplesFromFile(allocator: std.mem.Allocator, file: std.fs.File) ![]f64 {
|
||||
try file.seekTo(0);
|
||||
const byte_count = try file.getEndPos();
|
||||
assert(byte_count % 8 == 0);
|
||||
@ -132,10 +117,6 @@ fn readSamplesFromFile(allocator: Allocator, file: std.fs.File) ![]f64 {
|
||||
return samples;
|
||||
}
|
||||
|
||||
fn nanoToSeconds(ns: i128) f32 {
|
||||
return @as(f32, @floatFromInt(ns)) / std.time.ns_per_s;
|
||||
}
|
||||
|
||||
pub fn appendChannelFromFile(self: *App, file: std.fs.File) !void {
|
||||
const samples = try readSamplesFromFile(self.allocator, file);
|
||||
errdefer self.allocator.free(samples);
|
||||
@ -162,322 +143,204 @@ pub fn appendChannelFromFile(self: *App, file: std.fs.File) !void {
|
||||
});
|
||||
}
|
||||
|
||||
fn showChannelMinimap(self: *App, channel: *Channel, minimap_rect: rl.Rectangle) void {
|
||||
const MarkerState = enum {
|
||||
normal,
|
||||
hot,
|
||||
active
|
||||
};
|
||||
|
||||
var from_state = MarkerState.normal;
|
||||
var to_state = MarkerState.normal;
|
||||
var area_state = MarkerState.normal;
|
||||
|
||||
const sample_count = channel.samples.owned.len;
|
||||
const sample_count_f32: f32 = @floatFromInt(sample_count);
|
||||
|
||||
const grab_marker_width: f32 = @floatFromInt(self.grab_texture.normal.width);
|
||||
const view_column_min = RectUtils.left(minimap_rect) + grab_marker_width/2;
|
||||
const view_column_max = RectUtils.right(minimap_rect) - grab_marker_width/2;
|
||||
|
||||
const from_column_position = remap(f32,
|
||||
0,
|
||||
sample_count_f32,
|
||||
view_column_min,
|
||||
view_column_max,
|
||||
channel.view_rect.from
|
||||
);
|
||||
|
||||
const to_column_position = remap(f32,
|
||||
0,
|
||||
sample_count_f32,
|
||||
view_column_min,
|
||||
view_column_max,
|
||||
channel.view_rect.to
|
||||
);
|
||||
|
||||
const visible_area_rect = rl.Rectangle{
|
||||
.x = from_column_position,
|
||||
.y = minimap_rect.y,
|
||||
.width = to_column_position - from_column_position,
|
||||
.height = minimap_rect.height
|
||||
};
|
||||
|
||||
const mouse = self.ui.getMousePosition();
|
||||
if (RectUtils.isInsideVec2(minimap_rect, mouse)) {
|
||||
rl.drawRectangleLinesEx(minimap_rect, 1, rl.Color.gray);
|
||||
|
||||
const grab_distance = 20;
|
||||
|
||||
if (@abs(mouse.x - from_column_position) < grab_distance) {
|
||||
from_state = .hot;
|
||||
} else if (@abs(mouse.x - to_column_position) < grab_distance) {
|
||||
to_state = .hot;
|
||||
} else if (RectUtils.isInsideVec2(visible_area_rect, mouse)) {
|
||||
area_state = .hot;
|
||||
}
|
||||
|
||||
if (rl.isMouseButtonPressed(.mouse_button_left)) {
|
||||
if (area_state == .hot) {
|
||||
channel.dragged_marker = .both;
|
||||
} else if (from_state == .hot) {
|
||||
channel.dragged_marker = .from;
|
||||
} else if (to_state == .hot) {
|
||||
channel.dragged_marker = .to;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
rl.drawRectangleLinesEx(minimap_rect, 1, rl.Color.black);
|
||||
}
|
||||
|
||||
if (rl.isMouseButtonReleased(.mouse_button_left)) {
|
||||
channel.dragged_marker = null;
|
||||
}
|
||||
|
||||
const sample_under_mouse = remap(f32,
|
||||
view_column_min,
|
||||
view_column_max,
|
||||
0,
|
||||
sample_count_f32,
|
||||
mouse.x
|
||||
);
|
||||
|
||||
if (channel.dragged_marker) |marker| {
|
||||
const min_shown_samples = 1000;
|
||||
switch (marker) {
|
||||
.from => {
|
||||
from_state = .active;
|
||||
channel.view_rect.from = std.math.clamp(sample_under_mouse, 0, channel.view_rect.to-min_shown_samples);
|
||||
},
|
||||
.to => {
|
||||
to_state = .active;
|
||||
channel.view_rect.to = std.math.clamp(sample_under_mouse, channel.view_rect.from+min_shown_samples, @as(f32, sample_count_f32));
|
||||
},
|
||||
.both => {
|
||||
area_state = .active;
|
||||
from_state = .active;
|
||||
to_state = .active;
|
||||
|
||||
var delta = remap(f32,
|
||||
0,
|
||||
minimap_rect.width,
|
||||
0,
|
||||
sample_count_f32,
|
||||
self.ui.getMouseDelta().x
|
||||
);
|
||||
delta = std.math.clamp(delta, -channel.view_rect.from, sample_count_f32 - channel.view_rect.to);
|
||||
|
||||
channel.view_rect.from += delta;
|
||||
channel.view_rect.to += delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rl.drawRectangleRec(visible_area_rect, rl.Color.gray);
|
||||
|
||||
{
|
||||
const texture = switch (from_state) {
|
||||
.normal => self.grab_texture.normal,
|
||||
.hot => self.grab_texture.hot,
|
||||
.active => self.grab_texture.active
|
||||
};
|
||||
|
||||
rl.drawTextureV(
|
||||
texture,
|
||||
.{
|
||||
.x = from_column_position - @as(f32, @floatFromInt(texture.width)) / 2,
|
||||
.y = RectUtils.top(minimap_rect)
|
||||
},
|
||||
rl.Color.white
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const texture = switch (to_state) {
|
||||
.normal => self.grab_texture.normal,
|
||||
.hot => self.grab_texture.hot,
|
||||
.active => self.grab_texture.active
|
||||
};
|
||||
|
||||
rl.drawTextureV(
|
||||
texture,
|
||||
.{
|
||||
.x = to_column_position - @as(f32, @floatFromInt(texture.width)) / 2,
|
||||
.y = RectUtils.top(minimap_rect)
|
||||
},
|
||||
rl.Color.white
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const graph_height = 128;
|
||||
const minimap_height = 16;
|
||||
const channel_height = graph_height + minimap_height;
|
||||
fn showChannelRow(self: *App, channel: *Channel, channel_rect: rl.Rectangle) void {
|
||||
const graph_rect, const minimap_rect = RectUtils.horizontalSplit(channel_rect, graph_height);
|
||||
|
||||
{ // Graph
|
||||
Graph.draw(
|
||||
&self.ui,
|
||||
&channel.view_cache,
|
||||
graph_rect,
|
||||
channel.view_rect,
|
||||
channel.samples.owned
|
||||
);
|
||||
|
||||
rl.drawRectangleLinesEx(graph_rect, 1, rl.Color.black);
|
||||
}
|
||||
|
||||
self.showChannelMinimap(channel, minimap_rect);
|
||||
}
|
||||
|
||||
pub fn tick(self: *App) !void {
|
||||
// const dt = rl.getFrameTime();
|
||||
|
||||
rl.beginDrawing();
|
||||
defer rl.endDrawing();
|
||||
|
||||
rl.clearBackground(Theme.color_bg);
|
||||
|
||||
const window_width: f32 = @floatFromInt(rl.getScreenWidth());
|
||||
const window_height: f32 = @floatFromInt(rl.getScreenHeight());
|
||||
|
||||
const window_rect = rl.Rectangle.init(0, 0, window_width, window_height);
|
||||
|
||||
const split_x = 120;
|
||||
const controls_rect, const graphs_rect = RectUtils.verticalSplit(window_rect, split_x);
|
||||
_ = controls_rect;
|
||||
|
||||
rl.drawLineV(
|
||||
.{ .x = window_rect.x + split_x, .y = RectUtils.top(window_rect) },
|
||||
.{ .x = window_rect.x + split_x, .y = RectUtils.bottom(window_rect) },
|
||||
Theme.color_border
|
||||
);
|
||||
|
||||
if (showButton(&self.ui, @src(), .{
|
||||
.box = .{ .x = 10, .y = 10, .width = 100, .height = 100 },
|
||||
.text = "Load file"
|
||||
})) {
|
||||
if (Platform.openFilePicker()) |file| {
|
||||
defer file.close();
|
||||
|
||||
// TODO: Handle error
|
||||
try self.appendChannelFromFile(file);
|
||||
} else |err| {
|
||||
// TODO: Show error message to user;
|
||||
log.err("Failed to pick file: {}", .{ err });
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var channels_stack = UI.Stack.init(RectUtils.shrink(graphs_rect, 10), .top_to_bottom);
|
||||
for (self.channels.items) |*channel| {
|
||||
self.showChannelRow(channel, channels_stack.next(channel_height));
|
||||
}
|
||||
}
|
||||
|
||||
// rl.drawLineV(
|
||||
// Vec2.init(0, window_height/2),
|
||||
// Vec2.init(window_width, window_height/2),
|
||||
// rl.Color.gray
|
||||
// );
|
||||
|
||||
// if (self.channel_samples) |channel_samples| {
|
||||
// channel_samples.mutex.lock();
|
||||
// for (0.., channel_samples.samples) |channel_index, samples| {
|
||||
// const channel = self.channels.items[channel_index];
|
||||
|
||||
// Graph.draw(
|
||||
// rl.Rectangle{
|
||||
// .x = 20,
|
||||
// .y = 20,
|
||||
// .width = window_width - 40,
|
||||
// .height = window_height - 40
|
||||
// },
|
||||
// .{
|
||||
// .from = self.view_from,
|
||||
// .to = self.view_from + self.view_width,
|
||||
// .min_value = channel.min_sample,
|
||||
// .max_value = channel.max_sample
|
||||
// },
|
||||
// samples.items
|
||||
// );
|
||||
// }
|
||||
// channel_samples.mutex.unlock();
|
||||
// }
|
||||
|
||||
// drawGraph(
|
||||
// rl.Rectangle{
|
||||
// .x = 100,
|
||||
// .y = 20,
|
||||
// .width = window_width - 200,
|
||||
// .height = window_height - 40
|
||||
// },
|
||||
// .{
|
||||
// .from = view_from,
|
||||
// .to = view_from + view_width,
|
||||
// .min_value = -1,
|
||||
// .max_value = 1
|
||||
// },
|
||||
// example_samples1
|
||||
// );
|
||||
|
||||
// const move_speed = self.view_width * 0.25;
|
||||
// if (rl.isKeyDown(.key_d)) {
|
||||
// self.view_from += move_speed * dt;
|
||||
// }
|
||||
// if (rl.isKeyDown(.key_a)) {
|
||||
// self.view_from -= move_speed * dt;
|
||||
// }
|
||||
|
||||
// const zoom_speed = 0.5;
|
||||
// if (rl.isKeyDown(.key_w)) {
|
||||
// self.view_width *= (1 - zoom_speed * dt);
|
||||
// }
|
||||
// if (rl.isKeyDown(.key_s)) {
|
||||
// self.view_width *= (1 + zoom_speed * dt);
|
||||
// }
|
||||
rl.clearBackground(srcery.black);
|
||||
|
||||
if (rl.isKeyPressed(rl.KeyboardKey.key_f3)) {
|
||||
Platform.toggleConsoleWindow();
|
||||
}
|
||||
|
||||
// const now_ns = std.time.nanoTimestamp();
|
||||
// const now_since_start = nanoToSeconds(now_ns - self.start_time_ns);
|
||||
// const now_since_samping_start = nanoToSeconds(now_ns - self.channel_samples.started_sampling_ns.?);
|
||||
|
||||
{
|
||||
// const font_face = self.font_face;
|
||||
// const allocator = self.allocator;
|
||||
self.ui.begin();
|
||||
defer self.ui.end();
|
||||
self.ui.getParent().?.layout_axis = .Y;
|
||||
|
||||
// var y: f32 = 10;
|
||||
// try font_face.drawTextAlloc(allocator, "Time: {d:.03}", .{now_since_start}, Vec2.init(10, y), rl.Color.black);
|
||||
// y += 10;
|
||||
{
|
||||
const toolbar = self.ui.newBoxFromString("Toolbar");
|
||||
toolbar.flags.insert(.clickable);
|
||||
toolbar.background = rl.Color.green;
|
||||
toolbar.layout_axis = .X;
|
||||
toolbar.size = .{
|
||||
.x = UI.Size.percent(1, 0),
|
||||
.y = UI.Size.pixels(32, 1),
|
||||
};
|
||||
self.ui.pushParent(toolbar);
|
||||
defer self.ui.popParent();
|
||||
|
||||
// try font_face.drawTextAlloc(allocator, "View from: {d:.03}", .{self.view_from}, Vec2.init(10, y), rl.Color.black);
|
||||
// y += 10;
|
||||
{
|
||||
const box = self.ui.newBoxFromString("Add from file");
|
||||
box.flags.insert(.clickable);
|
||||
box.background = rl.Color.red;
|
||||
box.size = .{
|
||||
.x = UI.Size.text(2, 1),
|
||||
.y = UI.Size.percent(1, 1)
|
||||
};
|
||||
box.setText("Add from file", .text);
|
||||
|
||||
// try font_face.drawTextAlloc(allocator, "View width: {d:.03}", .{self.view_width}, Vec2.init(10, y), rl.Color.black);
|
||||
// y += 10;
|
||||
const signal = self.ui.signalFromBox(box);
|
||||
if (signal.clicked()) {
|
||||
if (Platform.openFilePicker()) |file| {
|
||||
defer file.close();
|
||||
|
||||
// try font_face.drawTextAlloc(allocator, "Dropped samples: {d:.03}", .{self.task_pool.droppedSamples()}, Vec2.init(10, y), rl.Color.black);
|
||||
// y += 10;
|
||||
// TODO: Handle error
|
||||
self.appendChannelFromFile(file) catch @panic("Failed to append channel from file");
|
||||
} else |err| {
|
||||
// TODO: Show error message to user;
|
||||
log.err("Failed to pick file: {}", .{ err });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for (0..self.channels.items.len) |i| {
|
||||
// const sample_count = channel_samples.samples[i].items.len;
|
||||
// y += 10;
|
||||
{
|
||||
const box = self.ui.newBoxFromString("Add from device");
|
||||
box.flags.insert(.clickable);
|
||||
box.background = rl.Color.lime;
|
||||
box.size = .{
|
||||
.x = UI.Size.text(2, 1),
|
||||
.y = UI.Size.percent(1, 1)
|
||||
};
|
||||
box.setText("Add from device", .text);
|
||||
|
||||
// try font_face.drawTextAlloc(allocator, "Channel {}:", .{i + 1}, Vec2.init(10, y), rl.Color.black);
|
||||
// y += 10;
|
||||
const signal = self.ui.signalFromBox(box);
|
||||
if (signal.clicked()) {
|
||||
std.debug.print("click two!\n", .{});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// try font_face.drawTextAlloc(allocator, "Sample count: {}", .{sample_count}, Vec2.init(20, y), rl.Color.black);
|
||||
// y += 10;
|
||||
{
|
||||
const rows_container = self.ui.newBoxFromString("Channels");
|
||||
rows_container.layout_axis = .Y;
|
||||
rows_container.size = .{
|
||||
.x = UI.Size.percent(1, 1),
|
||||
.y = UI.Size.percent(1, 0),
|
||||
};
|
||||
self.ui.pushParent(rows_container);
|
||||
defer self.ui.popParent();
|
||||
|
||||
// try font_face.drawTextAlloc(allocator, "Sample rate: {d:.03}", .{@as(f64, @floatFromInt(sample_count)) / now_since_samping_start}, Vec2.init(20, y), rl.Color.black);
|
||||
// y += 10;
|
||||
// }
|
||||
for (self.channels.slice()) |*_channel| {
|
||||
const channel: *Channel = _channel;
|
||||
|
||||
const channel_box = self.ui.newBoxFromPtr(channel);
|
||||
channel_box.background = rl.Color.blue;
|
||||
channel_box.layout_axis = .Y;
|
||||
channel_box.size.x = UI.Size.percent(1, 0);
|
||||
channel_box.size.y = UI.Size.childrenSum(1);
|
||||
self.ui.pushParent(channel_box);
|
||||
defer self.ui.popParent();
|
||||
|
||||
const graph_box = self.ui.newBoxFromString("Graph");
|
||||
graph_box.background = rl.Color.blue;
|
||||
graph_box.layout_axis = .Y;
|
||||
graph_box.size.x = UI.Size.percent(1, 0);
|
||||
graph_box.size.y = UI.Size.pixels(256, 1);
|
||||
|
||||
Graph.drawCached(&channel.view_cache, graph_box.persistent.size, channel.view_rect, channel.samples.owned);
|
||||
if (channel.view_cache.texture) |texture| {
|
||||
graph_box.texture = texture.texture;
|
||||
}
|
||||
|
||||
{
|
||||
const sample_count: f32 = @floatFromInt(channel.samples.owned.len);
|
||||
const min_visible_samples = sample_count*0.02;
|
||||
|
||||
const minimap_box = self.ui.newBoxFromString("Minimap");
|
||||
minimap_box.background = rl.Color.dark_purple;
|
||||
minimap_box.layout_axis = .X;
|
||||
minimap_box.size.x = UI.Size.percent(1, 0);
|
||||
minimap_box.size.y = UI.Size.pixels(32, 1);
|
||||
self.ui.pushParent(minimap_box);
|
||||
defer self.ui.popParent();
|
||||
|
||||
const minimap_rect = minimap_box.computedRect();
|
||||
|
||||
{
|
||||
const middle_box = self.ui.newBoxFromString("Middle knob");
|
||||
middle_box.flags.insert(.clickable);
|
||||
middle_box.flags.insert(.draggable);
|
||||
middle_box.background = rl.Color.ray_white;
|
||||
middle_box.size.y = UI.Size.pixels(32, 1);
|
||||
|
||||
const signal = self.ui.signalFromBox(middle_box);
|
||||
if (signal.dragged()) {
|
||||
|
||||
var samples_moved = signal.drag.x / minimap_rect.width * sample_count;
|
||||
|
||||
samples_moved = clamp(samples_moved, -channel.view_rect.from, sample_count - channel.view_rect.to);
|
||||
|
||||
channel.view_rect.from += samples_moved;
|
||||
channel.view_rect.to += samples_moved;
|
||||
}
|
||||
|
||||
middle_box.override_x = minimap_rect.width * channel.view_rect.from / sample_count + 4;
|
||||
middle_box.size.x = UI.Size.pixels(minimap_rect.width * (channel.view_rect.to - channel.view_rect.from) / sample_count - 8, 1);
|
||||
}
|
||||
|
||||
{
|
||||
const left_knob_box = self.ui.newBoxFromString("Left knob");
|
||||
left_knob_box.flags.insert(.clickable);
|
||||
left_knob_box.flags.insert(.draggable);
|
||||
left_knob_box.background = rl.Color.ray_white;
|
||||
left_knob_box.size.x = UI.Size.pixels(8, 1);
|
||||
left_knob_box.size.y = UI.Size.pixels(32, 1);
|
||||
|
||||
const left_signal = self.ui.signalFromBox(left_knob_box);
|
||||
if (left_signal.dragged()) {
|
||||
channel.view_rect.from += remap(
|
||||
f32,
|
||||
0, minimap_rect.width,
|
||||
0, sample_count,
|
||||
left_signal.drag.x
|
||||
);
|
||||
|
||||
channel.view_rect.from = clamp(channel.view_rect.from, 0, channel.view_rect.to-min_visible_samples);
|
||||
}
|
||||
|
||||
left_knob_box.override_x = minimap_rect.width * channel.view_rect.from / sample_count - left_knob_box.persistent.size.x/2;
|
||||
}
|
||||
|
||||
{
|
||||
const right_knob_box = self.ui.newBoxFromString("Right knobaaa");
|
||||
right_knob_box.flags.insert(.clickable);
|
||||
right_knob_box.flags.insert(.draggable);
|
||||
right_knob_box.background = rl.Color.ray_white;
|
||||
right_knob_box.size.x = UI.Size.pixels(8, 1);
|
||||
right_knob_box.size.y = UI.Size.pixels(32, 1);
|
||||
|
||||
const right_signal = self.ui.signalFromBox(right_knob_box);
|
||||
if (right_signal.dragged()) {
|
||||
channel.view_rect.to += remap(
|
||||
f32,
|
||||
0, minimap_rect.width,
|
||||
0, sample_count,
|
||||
right_signal.drag.x
|
||||
);
|
||||
|
||||
channel.view_rect.to = clamp(channel.view_rect.to, channel.view_rect.from+min_visible_samples, sample_count);
|
||||
}
|
||||
|
||||
right_knob_box.override_x = minimap_rect.width * channel.view_rect.to / sample_count - right_knob_box.persistent.size.x/2;
|
||||
}
|
||||
}
|
||||
|
||||
// const graph_widget = self.ui.newWidget(self.ui.keyFromString("samples-plot"));
|
||||
// graph_widget.size.y = .{ .pixels = channel.height };
|
||||
// graph_widget.size.x = .{ .percent = 1 };
|
||||
// graph_widget.graph = .{
|
||||
// .cache = &channel.view_cache,
|
||||
// .options = channel.view_rect,
|
||||
// .samples = channel.samples.owned
|
||||
// };
|
||||
|
||||
// const minimap_widget = self.showChannelMinimap(channel);
|
||||
// minimap_widget.size.y = .fit_children;
|
||||
// minimap_widget.size.x = .{ .percent = 1 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rl.drawFPS(@as(i32, @intFromFloat(window_width)) - 100, 10);
|
||||
self.ui.draw();
|
||||
}
|
88
src/assets.zig
Normal file
88
src/assets.zig
Normal file
@ -0,0 +1,88 @@
|
||||
const std = @import("std");
|
||||
const rl = @import("raylib");
|
||||
const srcery = @import("./srcery.zig");
|
||||
const FontFace = @import("./font-face.zig");
|
||||
const Aseprite = @import("./aseprite.zig");
|
||||
|
||||
const assert = std.debug.assert;
|
||||
|
||||
pub const FontId = enum {
|
||||
text
|
||||
};
|
||||
|
||||
var loaded_fonts: std.BoundedArray(rl.Font, 32) = .{};
|
||||
|
||||
const FontArray = std.EnumArray(FontId, FontFace);
|
||||
var fonts: FontArray = FontArray.initUndefined();
|
||||
|
||||
pub var grab_texture: struct {
|
||||
normal: rl.Texture2D,
|
||||
hot: rl.Texture2D,
|
||||
active: rl.Texture2D,
|
||||
} = undefined;
|
||||
|
||||
pub fn font(font_id: FontId) FontFace {
|
||||
return fonts.get(font_id);
|
||||
}
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) !void {
|
||||
const roboto_regular = @embedFile("./assets/fonts/roboto/Roboto-Regular.ttf");
|
||||
|
||||
const default_font = try loadFont(roboto_regular, 16);
|
||||
|
||||
fonts = FontArray.init(.{
|
||||
.text = FontFace{ .font = default_font, .line_height = 1.2 }
|
||||
});
|
||||
|
||||
const grab_ase = try Aseprite.init(allocator, @embedFile("./assets/grab-marker.ase"));
|
||||
defer grab_ase.deinit();
|
||||
|
||||
const grab_normal_image = grab_ase.getTagImage(grab_ase.getTag("normal") orelse return error.TagNotFound);
|
||||
defer grab_normal_image.unload();
|
||||
const grab_normal_texture = rl.loadTextureFromImage(grab_normal_image);
|
||||
errdefer grab_normal_texture.unload();
|
||||
|
||||
const grab_hot_image = grab_ase.getTagImage(grab_ase.getTag("hot") orelse return error.TagNotFound);
|
||||
defer grab_hot_image.unload();
|
||||
const grab_hot_texture = rl.loadTextureFromImage(grab_hot_image);
|
||||
errdefer grab_hot_texture.unload();
|
||||
|
||||
const grab_active_image = grab_ase.getTagImage(grab_ase.getTag("active") orelse return error.TagNotFound);
|
||||
defer grab_active_image.unload();
|
||||
const grab_active_texture = rl.loadTextureFromImage(grab_active_image);
|
||||
errdefer grab_active_texture.unload();
|
||||
|
||||
grab_texture = .{
|
||||
.normal = grab_normal_texture,
|
||||
.hot = grab_hot_texture,
|
||||
.active = grab_active_texture
|
||||
};
|
||||
}
|
||||
|
||||
fn loadFont(ttf_data: []const u8, font_size: u32) !rl.Font {
|
||||
var codepoints: [95]i32 = undefined;
|
||||
for (0..codepoints.len) |i| {
|
||||
codepoints[i] = @as(i32, @intCast(i)) + 32;
|
||||
}
|
||||
|
||||
const loaded_font = rl.loadFontFromMemory(".ttf", ttf_data, @intCast(font_size), &codepoints);
|
||||
if (!loaded_font.isReady()) {
|
||||
return error.LoadFontFromMemory;
|
||||
}
|
||||
|
||||
loaded_fonts.appendAssumeCapacity(loaded_font);
|
||||
|
||||
return loaded_font;
|
||||
}
|
||||
|
||||
pub fn deinit(allocator: std.mem.Allocator) void {
|
||||
_ = allocator;
|
||||
|
||||
for (loaded_fonts.slice()) |loaded_font| {
|
||||
loaded_font.unload();
|
||||
}
|
||||
|
||||
grab_texture.active.unload();
|
||||
grab_texture.hot.unload();
|
||||
grab_texture.normal.unload();
|
||||
}
|
202
src/assets/fonts/roboto/LICENSE.txt
Normal file
202
src/assets/fonts/roboto/LICENSE.txt
Normal file
@ -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.
|
BIN
src/assets/fonts/roboto/Roboto-Regular.ttf
Normal file
BIN
src/assets/fonts/roboto/Roboto-Regular.ttf
Normal file
Binary file not shown.
@ -18,6 +18,10 @@ pub fn getSize(self: @This()) f32 {
|
||||
return @floatFromInt(self.font.baseSize);
|
||||
}
|
||||
|
||||
pub fn getLineSize(self: @This()) f32 {
|
||||
return self.getSize() * self.line_height;
|
||||
}
|
||||
|
||||
pub fn drawTextLines(self: @This(), lines: []const []const u8, position: rl.Vector2, tint: rl.Color) void {
|
||||
var offset_y: f32 = 0;
|
||||
|
||||
|
@ -1,10 +1,9 @@
|
||||
const builtin = @import("builtin");
|
||||
const std = @import("std");
|
||||
const rl = @import("raylib");
|
||||
const Theme = @import("../theme.zig");
|
||||
const UI = @import("./root.zig");
|
||||
const srcery = @import("./srcery.zig");
|
||||
|
||||
const remap = @import("../utils.zig").remap;
|
||||
const remap = @import("./utils.zig").remap;
|
||||
const assert = std.debug.assert;
|
||||
const Vec2 = rl.Vector2;
|
||||
const clamp = std.math.clamp;
|
||||
@ -23,7 +22,7 @@ pub const ViewOptions = struct {
|
||||
min_value: f64,
|
||||
max_value: f64,
|
||||
left_aligned: bool = true,
|
||||
color: rl.Color = Theme.color_graph,
|
||||
color: rl.Color = srcery.red,
|
||||
dot_size: f32 = 2
|
||||
};
|
||||
|
||||
@ -49,7 +48,7 @@ pub const Cache = struct {
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.width = @floatFromInt(texture.texture.width),
|
||||
.height = @floatFromInt(-texture.texture.height)
|
||||
.height = @floatFromInt(texture.texture.height)
|
||||
};
|
||||
rl.drawTexturePro(
|
||||
texture.texture,
|
||||
@ -97,7 +96,7 @@ fn clampIndexUsize(value: f32, size: usize) usize {
|
||||
return @intFromFloat(clamp(value, 0, size_f32));
|
||||
}
|
||||
|
||||
fn drawSamples(ui: *UI, draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
|
||||
fn drawSamples(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
|
||||
assert(options.left_aligned); // TODO:
|
||||
assert(options.to >= options.from);
|
||||
|
||||
@ -154,8 +153,13 @@ fn drawSamples(ui: *UI, draw_rect: rl.Rectangle, options: ViewOptions, samples:
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ui.beginScissorModeRect(draw_rect);
|
||||
defer ui.endScissorMode();
|
||||
rl.beginScissorMode(
|
||||
@intFromFloat(draw_rect.x),
|
||||
@intFromFloat(draw_rect.y),
|
||||
@intFromFloat(draw_rect.width),
|
||||
@intFromFloat(draw_rect.height),
|
||||
);
|
||||
defer rl.endScissorMode();
|
||||
|
||||
{
|
||||
const from_index = clampIndexUsize(@floor(options.from), samples.len);
|
||||
@ -185,56 +189,69 @@ fn drawSamples(ui: *UI, draw_rect: rl.Rectangle, options: ViewOptions, samples:
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw(ui: *UI, cache: ?*Cache, draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
|
||||
if (disable_caching) {
|
||||
drawSamples(ui, draw_rect, options, samples);
|
||||
pub fn drawCached(cache: *Cache, render_size: Vec2, options: ViewOptions, samples: []const f64) void {
|
||||
const render_width: i32 = @intFromFloat(@ceil(render_size.x));
|
||||
const render_height: i32 = @intFromFloat(@ceil(render_size.y));
|
||||
|
||||
if (render_width <= 0 or render_height <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cache) |c| {
|
||||
const render_width: i32 = @intFromFloat(@ceil(draw_rect.width));
|
||||
const render_height: i32 = @intFromFloat(@ceil(draw_rect.height));
|
||||
|
||||
// Unload render texture if rendering width or height changed
|
||||
if (c.texture) |render_texture| {
|
||||
const texure = render_texture.texture;
|
||||
if (texure.width != render_width or texure.height != render_height) {
|
||||
render_texture.unload();
|
||||
c.texture = null;
|
||||
c.options = null;
|
||||
}
|
||||
// Unload render texture if rendering width or height changed
|
||||
if (cache.texture) |render_texture| {
|
||||
const texure = render_texture.texture;
|
||||
if (texure.width != render_width or texure.height != render_height) {
|
||||
render_texture.unload();
|
||||
cache.texture = null;
|
||||
cache.options = null;
|
||||
}
|
||||
|
||||
if (c.texture == null) {
|
||||
const texture = rl.loadRenderTexture(render_width, render_height);
|
||||
// TODO: Maybe fallback to just drawing without caching, if GPU doesn't have enough memory?
|
||||
assert(rl.isRenderTextureReady(texture));
|
||||
c.texture = texture;
|
||||
}
|
||||
|
||||
const render_texture = c.texture.?;
|
||||
|
||||
if (c.options != null and std.meta.eql(c.options.?, options)) {
|
||||
c.draw(draw_rect);
|
||||
return;
|
||||
}
|
||||
|
||||
c.options = options;
|
||||
render_texture.begin();
|
||||
|
||||
ui.pushTransform();
|
||||
ui.transformTranslate(-draw_rect.x, -draw_rect.y);
|
||||
rl.clearBackground(rl.Color.black.alpha(0));
|
||||
}
|
||||
|
||||
drawSamples(ui, draw_rect, options, samples);
|
||||
if (cache.texture == null) {
|
||||
const texture = rl.loadRenderTexture(render_width, render_height);
|
||||
// TODO: Maybe fallback to just drawing without caching, if GPU doesn't have enough memory?
|
||||
assert(rl.isRenderTextureReady(texture));
|
||||
cache.texture = texture;
|
||||
}
|
||||
|
||||
if (cache) |c| {
|
||||
ui.popTransform();
|
||||
const render_texture = cache.texture.?;
|
||||
|
||||
const render_texture = c.texture.?;
|
||||
render_texture.end();
|
||||
if (cache.options != null and std.meta.eql(cache.options.?, options)) {
|
||||
// Cached graph hasn't changed, no need to redraw.
|
||||
return;
|
||||
}
|
||||
|
||||
cache.options = options;
|
||||
|
||||
render_texture.begin();
|
||||
defer render_texture.end();
|
||||
|
||||
rl.gl.rlPushMatrix();
|
||||
defer rl.gl.rlPopMatrix();
|
||||
|
||||
rl.clearBackground(rl.Color.black.alpha(0));
|
||||
rl.gl.rlTranslatef(0, render_size.y, 0);
|
||||
rl.gl.rlScalef(1, -1, 1);
|
||||
|
||||
const draw_rect = rl.Rectangle{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.width = render_size.x,
|
||||
.height = render_size.y
|
||||
};
|
||||
drawSamples(draw_rect, options, samples);
|
||||
}
|
||||
|
||||
pub fn draw(cache: ?*Cache, draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
|
||||
if (draw_rect.width < 0 or draw_rect.height < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cache != null and !disable_caching) {
|
||||
const c = cache.?;
|
||||
drawCached(c, .{ .x = draw_rect.width, .y = draw_rect.height }, options, samples);
|
||||
c.draw(draw_rect);
|
||||
} else {
|
||||
drawSamples(draw_rect, options, samples);
|
||||
}
|
||||
}
|
57
src/main.zig
57
src/main.zig
@ -2,7 +2,7 @@ const std = @import("std");
|
||||
const rl = @import("raylib");
|
||||
const builtin = @import("builtin");
|
||||
const Application = @import("./app.zig");
|
||||
const Theme = @import("./theme.zig");
|
||||
const Assets = @import("./assets.zig");
|
||||
const raylib_h = @cImport({
|
||||
@cInclude("stdio.h");
|
||||
@cInclude("raylib.h");
|
||||
@ -10,6 +10,14 @@ const raylib_h = @cImport({
|
||||
|
||||
const log = std.log;
|
||||
|
||||
// TODO: Maybe move this to a config.zig or options.zig file.
|
||||
// Have all of the contstants in a single file.
|
||||
pub const version = std.SemanticVersion{
|
||||
.major = 0,
|
||||
.minor = 1,
|
||||
.patch = 0
|
||||
};
|
||||
|
||||
fn toRaylibLogLevel(log_level: log.Level) rl.TraceLogLevel {
|
||||
return switch (log_level) {
|
||||
.err => rl.TraceLogLevel.log_error,
|
||||
@ -62,30 +70,6 @@ pub fn main() !void {
|
||||
|
||||
// const devices = try ni_daq.listDeviceNames();
|
||||
|
||||
// log.info("NI-DAQ version: {}", .{try NIDaq.version()});
|
||||
|
||||
// std.debug.print("Devices ({}):\n", .{devices.len});
|
||||
// for (devices) |device| {
|
||||
// std.debug.print(" * '{s}' ({})\n", .{device, device.len});
|
||||
|
||||
// const analog_inputs = try ni_daq.listDeviceAIPhysicalChannels(device);
|
||||
// for (analog_inputs) |channel_name| {
|
||||
// std.debug.print(" * '{s}' (Analog input)\n", .{channel_name});
|
||||
// }
|
||||
|
||||
// for (try ni_daq.listDeviceAOPhysicalChannels(device)) |channel_name| {
|
||||
// std.debug.print(" * '{s}' (Analog output)\n", .{channel_name});
|
||||
// }
|
||||
|
||||
// for (try ni_daq.listDeviceCOPhysicalChannels(device)) |channel_name| {
|
||||
// std.debug.print(" * '{s}' (Counter output)\n", .{channel_name});
|
||||
// }
|
||||
|
||||
// for (try ni_daq.listDeviceCIPhysicalChannels(device)) |channel_name| {
|
||||
// std.debug.print(" * '{s}' (Counter input)\n", .{channel_name});
|
||||
// }
|
||||
// }
|
||||
|
||||
// for (devices) |device| {
|
||||
// if (try ni_daq.checkDeviceAIMeasurementType(device, .Voltage)) {
|
||||
// const voltage_ranges = try ni_daq.listDeviceAIVoltageRanges(device);
|
||||
@ -147,31 +131,17 @@ pub fn main() !void {
|
||||
rl.initWindow(800, 450, "DAQ view");
|
||||
defer rl.closeWindow();
|
||||
rl.setWindowState(.{ .window_resizable = true, .vsync_hint = true });
|
||||
rl.setWindowMinSize(256, 256);
|
||||
rl.setWindowIcon(icon_image);
|
||||
|
||||
if (builtin.mode != .Debug) {
|
||||
rl.setExitKey(.key_null);
|
||||
}
|
||||
|
||||
try Theme.init();
|
||||
defer Theme.deinit();
|
||||
try Assets.init(allocator);
|
||||
defer Assets.deinit(allocator);
|
||||
|
||||
var app = try Application.init(
|
||||
allocator,
|
||||
.{
|
||||
.max_tasks = 32, // devices.len * 2,
|
||||
.max_channels = 64
|
||||
},
|
||||
.{
|
||||
.max_devices = 4,
|
||||
.max_analog_inputs = 32,
|
||||
.max_analog_outputs = 8,
|
||||
.max_counter_outputs = 8,
|
||||
.max_counter_inputs = 8,
|
||||
.max_analog_input_voltage_ranges = 4,
|
||||
.max_analog_output_voltage_ranges = 4
|
||||
}
|
||||
);
|
||||
var app = try Application.init(allocator);
|
||||
defer app.deinit();
|
||||
|
||||
if (builtin.mode == .Debug) {
|
||||
@ -188,7 +158,6 @@ pub fn main() !void {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
while (!rl.windowShouldClose()) {
|
||||
try app.tick();
|
||||
}
|
||||
|
@ -368,6 +368,7 @@ const DeviceBuffers = struct {
|
||||
}
|
||||
};
|
||||
|
||||
options: Options,
|
||||
device_names_buffer: []u8,
|
||||
device_names: StringArrayListUnmanaged,
|
||||
|
||||
@ -390,6 +391,7 @@ pub fn init(allocator: std.mem.Allocator, options: Options) !NIDaq {
|
||||
}
|
||||
|
||||
return NIDaq{
|
||||
.options = options,
|
||||
.device_names_buffer = device_names_buffer,
|
||||
.device_names = device_names,
|
||||
.device_buffers = device_buffers
|
||||
|
@ -1,40 +0,0 @@
|
||||
const std = @import("std");
|
||||
const rl = @import("raylib");
|
||||
const srcery = @import("./srcery.zig");
|
||||
const FontFace = @import("./font-face.zig");
|
||||
|
||||
const assert = std.debug.assert;
|
||||
|
||||
// TODO: Maybe don't have this as a global? Pass the theme around where it is needed.
|
||||
//
|
||||
// But for now, having it as a global is very convenient.
|
||||
|
||||
pub const FontId = enum {
|
||||
text
|
||||
};
|
||||
|
||||
const FontArray = std.EnumArray(FontId, FontFace);
|
||||
var fonts: FontArray = FontArray.initUndefined();
|
||||
|
||||
pub const color_bg = srcery.black;
|
||||
pub const color_border = srcery.bright_black;
|
||||
pub const color_button = srcery.xgray7;
|
||||
pub const color_text = srcery.bright_white;
|
||||
pub const color_graph = srcery.red;
|
||||
|
||||
pub fn font(font_id: FontId) FontFace {
|
||||
return fonts.get(font_id);
|
||||
}
|
||||
|
||||
pub fn init() !void {
|
||||
const default_font = rl.getFontDefault();
|
||||
assert(default_font.isReady());
|
||||
|
||||
fonts = FontArray.init(.{
|
||||
.text = FontFace{ .font = default_font }
|
||||
});
|
||||
}
|
||||
|
||||
pub fn deinit() void {
|
||||
// TODO: Deinit fonts
|
||||
}
|
954
src/ui.zig
Normal file
954
src/ui.zig
Normal file
@ -0,0 +1,954 @@
|
||||
const std = @import("std");
|
||||
const rl = @import("raylib");
|
||||
const Assets = @import("./assets.zig");
|
||||
const rect_utils = @import("./rect-utils.zig");
|
||||
const srcery = @import("./srcery.zig");
|
||||
|
||||
const log = std.lgo.scoped(.ui);
|
||||
const assert = std.debug.assert;
|
||||
const Vec2 = rl.Vector2;
|
||||
const Rect = rl.Rectangle;
|
||||
|
||||
const UI = @This();
|
||||
|
||||
const debug = false;
|
||||
const max_boxes = 128;
|
||||
const max_events = 256;
|
||||
|
||||
const RectFormatted = struct {
|
||||
rect: ?Rect,
|
||||
|
||||
pub fn init(rect: ?Rect) RectFormatted {
|
||||
return RectFormatted{
|
||||
.rect = rect
|
||||
};
|
||||
}
|
||||
|
||||
pub fn format(
|
||||
self: RectFormatted,
|
||||
comptime fmt: []const u8,
|
||||
options: std.fmt.FormatOptions,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
_ = fmt;
|
||||
_ = options;
|
||||
|
||||
if (self.rect) |rect| {
|
||||
try writer.print("Rect{{ {d:.02}, {d:.02}, {d:.02}, {d:.02} }}", .{ rect.x, rect.y, rect.width, rect.height });
|
||||
} else {
|
||||
try writer.print("{}", .{ null });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const Axis = enum {
|
||||
X,
|
||||
Y,
|
||||
|
||||
fn flip(self: Axis) Axis {
|
||||
return switch (self) {
|
||||
.X => .Y,
|
||||
.Y => .X
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Size = struct {
|
||||
kind: union(enum) {
|
||||
pixels: f32,
|
||||
percent: f32,
|
||||
text: f32,
|
||||
children_sum,
|
||||
},
|
||||
strictness: f32 = 1,
|
||||
|
||||
pub fn pixels(amount: f32, strictness: f32) Size {
|
||||
return Size{
|
||||
.kind = .{ .pixels = amount },
|
||||
.strictness = strictness
|
||||
};
|
||||
}
|
||||
|
||||
pub fn text(padding: f32, strictness: f32) Size {
|
||||
return Size{
|
||||
.kind = .{ .text = padding },
|
||||
.strictness = strictness
|
||||
};
|
||||
}
|
||||
|
||||
pub fn percent(amount: f32, strictness: f32) Size {
|
||||
return Size{
|
||||
.kind = .{ .percent = amount },
|
||||
.strictness = strictness
|
||||
};
|
||||
}
|
||||
|
||||
pub fn childrenSum(strictness: f32) Size {
|
||||
return Size{
|
||||
.kind = .children_sum,
|
||||
.strictness = strictness
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Vec2Size = struct {
|
||||
x: Size,
|
||||
y: Size,
|
||||
|
||||
pub fn zero() Vec2Size {
|
||||
return Vec2Size{
|
||||
.x = Size.pixels(0, 1),
|
||||
.y = Size.pixels(0, 1)
|
||||
};
|
||||
}
|
||||
|
||||
inline fn getAxis(self: *Vec2Size, axis: Axis) *Size {
|
||||
return switch (axis) {
|
||||
.X => &self.x,
|
||||
.Y => &self.y
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Key = struct {
|
||||
hash: u64 = 0,
|
||||
|
||||
pub fn initPtr(ptr: anytype) Key {
|
||||
return Key.initUsize(@intFromPtr(ptr));
|
||||
}
|
||||
|
||||
pub fn initUsize(num: usize) Key {
|
||||
return Key{
|
||||
.hash = @truncate(num)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn initString(seed: u64, text: []const u8) Key {
|
||||
return Key{
|
||||
.hash = std.hash.XxHash3.hash(seed, text)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn initNil() Key {
|
||||
return Key{ .hash = 0 };
|
||||
}
|
||||
|
||||
pub fn eql(self: Key, other: Key) bool {
|
||||
return self.hash == other.hash;
|
||||
}
|
||||
|
||||
pub fn isNil(self: Key) bool {
|
||||
return self.hash == 0;
|
||||
}
|
||||
|
||||
pub fn format(
|
||||
self: Key,
|
||||
comptime fmt: []const u8,
|
||||
options: std.fmt.FormatOptions,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
_ = fmt;
|
||||
_ = options;
|
||||
|
||||
try writer.print("Key{{ 0x{x} }}", .{ self.hash });
|
||||
}
|
||||
};
|
||||
|
||||
pub const Event = union(enum) {
|
||||
mouse_pressed: rl.MouseButton,
|
||||
mouse_released: rl.MouseButton,
|
||||
mouse_move: Vec2
|
||||
};
|
||||
|
||||
pub const Signal = struct {
|
||||
pub const Flag = enum {
|
||||
left_pressed,
|
||||
right_pressed,
|
||||
|
||||
left_released,
|
||||
right_released,
|
||||
|
||||
left_clicked,
|
||||
right_clicked,
|
||||
|
||||
left_dragging,
|
||||
right_dragging
|
||||
};
|
||||
|
||||
flags: std.EnumSet(Flag) = .{},
|
||||
drag: Vec2 = .{ .x = 0, .y = 0 },
|
||||
|
||||
pub fn clicked(self: Signal) bool {
|
||||
return self.flags.contains(.left_clicked) or self.flags.contains(.right_clicked);
|
||||
}
|
||||
|
||||
pub fn dragged(self: Signal) bool {
|
||||
return self.flags.contains(.left_dragging) or self.flags.contains(.right_dragging);
|
||||
}
|
||||
|
||||
fn insertMousePressed(self: *Signal, mouse_button: rl.MouseButton) void {
|
||||
if (mouse_button == .mouse_button_left) {
|
||||
self.flags.insert(.left_pressed);
|
||||
} else if (mouse_button == .mouse_button_right) {
|
||||
self.flags.insert(.right_pressed);
|
||||
}
|
||||
}
|
||||
|
||||
fn insertMouseReleased(self: *Signal, mouse_button: rl.MouseButton) void {
|
||||
if (mouse_button == .mouse_button_left) {
|
||||
self.flags.insert(.left_released);
|
||||
} else if (mouse_button == .mouse_button_right) {
|
||||
self.flags.insert(.right_released);
|
||||
}
|
||||
}
|
||||
|
||||
fn insertMouseClicked(self: *Signal, mouse_button: rl.MouseButton) void {
|
||||
if (mouse_button == .mouse_button_left) {
|
||||
self.flags.insert(.left_clicked);
|
||||
} else if (mouse_button == .mouse_button_right) {
|
||||
self.flags.insert(.right_clicked);
|
||||
}
|
||||
}
|
||||
|
||||
fn insertMouseDragged(self: *Signal, mouse_button: rl.MouseButton) void {
|
||||
if (mouse_button == .mouse_button_left) {
|
||||
self.flags.insert(.left_dragging);
|
||||
} else if (mouse_button == .mouse_button_right) {
|
||||
self.flags.insert(.right_dragging);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const BoxIndex = std.math.IntFittingRange(0, max_boxes);
|
||||
|
||||
pub const Box = struct {
|
||||
pub const Persistent = struct {
|
||||
size: Vec2 = .{ .x = 0, .y = 0 },
|
||||
position: Vec2 = .{ .x = 0, .y = 0 },
|
||||
};
|
||||
|
||||
pub const Flag = enum {
|
||||
clickable,
|
||||
draggable
|
||||
};
|
||||
|
||||
pub const Flags = std.EnumSet(Flag);
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
key: Key,
|
||||
size: Vec2Size = Vec2Size.zero(),
|
||||
flags: Flags = .{},
|
||||
override_x: ?f32 = null,
|
||||
override_y: ?f32 = null,
|
||||
background: ?rl.Color = null,
|
||||
rounded: bool = false,
|
||||
layout_axis: Axis = .X,
|
||||
last_used_frame: u64 = 0,
|
||||
text: ?struct {
|
||||
content: []u8,
|
||||
font: Assets.FontId,
|
||||
color: rl.Color = srcery.bright_white
|
||||
} = null,
|
||||
texture: ?rl.Texture2D = null,
|
||||
|
||||
persistent: Persistent = .{},
|
||||
|
||||
// Fields for maintaining tree data structure
|
||||
|
||||
// Index of this box
|
||||
index: BoxIndex,
|
||||
// Go down the tree to the first child
|
||||
first_child_index: ?BoxIndex = null,
|
||||
// Go down the tree to the last child
|
||||
last_child_index: ?BoxIndex = null,
|
||||
// Go up the tree
|
||||
parent_index: ?BoxIndex = null,
|
||||
// Go the next node on the same level
|
||||
next_sibling_index: ?BoxIndex = null,
|
||||
|
||||
pub fn computedRect(self: *Box) Rect {
|
||||
return Rect{
|
||||
.x = self.persistent.position.x,
|
||||
.y = self.persistent.position.y,
|
||||
.width = self.persistent.size.x,
|
||||
.height = self.persistent.size.y
|
||||
};
|
||||
}
|
||||
|
||||
pub fn setText(self: *Box, text: []const u8, font: Assets.FontId) void {
|
||||
self.text = .{
|
||||
.content = self.allocator.dupe(u8, text) catch return,
|
||||
.font = font
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const BoxChildIterator = struct {
|
||||
current_child: ?BoxIndex,
|
||||
boxes: []Box,
|
||||
|
||||
pub fn next(self: *BoxChildIterator) ?*Box {
|
||||
if (self.current_child) |child_index| {
|
||||
const child = &self.boxes[child_index];
|
||||
self.current_child = child.next_sibling_index;
|
||||
return child;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const BoxParentIterator = struct {
|
||||
current_parent: ?BoxIndex,
|
||||
boxes: []Box,
|
||||
|
||||
pub fn next(self: *BoxParentIterator) ?*Box {
|
||||
if (self.current_parent) |parent_index| {
|
||||
const parent = &self.boxes[parent_index];
|
||||
self.current_parent = parent.parent_index;
|
||||
return parent;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub const root_box_key = Key.initString(0, "$root$");
|
||||
|
||||
arenas: [2]std.heap.ArenaAllocator,
|
||||
|
||||
boxes: std.BoundedArray(Box, max_boxes) = .{},
|
||||
parent_index_stack: std.BoundedArray(BoxIndex, max_boxes) = .{},
|
||||
|
||||
frame_index: u64 = 0,
|
||||
hot_box_key: ?Key = null,
|
||||
active_box_keys: std.EnumMap(rl.MouseButton, Key) = .{},
|
||||
|
||||
events: std.BoundedArray(Event, max_events) = .{},
|
||||
mouse: Vec2 = .{ .x = 0, .y = 0 },
|
||||
mouse_delta: Vec2 = .{ .x = 0, .y = 0 },
|
||||
mouse_buttons: std.EnumSet(rl.MouseButton) = .{},
|
||||
window_size: Vec2 = .{ .x = 0, .y = 0 },
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) UI {
|
||||
return UI{
|
||||
.arenas = .{ std.heap.ArenaAllocator.init(allocator), std.heap.ArenaAllocator.init(allocator) },
|
||||
.mouse = rl.getMousePosition()
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *UI) void {
|
||||
self.arenas[0].deinit();
|
||||
self.arenas[1].deinit();
|
||||
}
|
||||
|
||||
pub fn begin(self: *UI) void {
|
||||
const window_width = rl.getScreenWidth();
|
||||
const window_height = rl.getScreenHeight();
|
||||
|
||||
const mouse = rl.getMousePosition();
|
||||
self.mouse_delta = mouse.subtract(self.mouse);
|
||||
|
||||
const active_box_flags = self.getActiveBoxFlags();
|
||||
if (active_box_flags.contains(.draggable)) {
|
||||
const mouse_x = rl.getMouseX();
|
||||
const mouse_y = rl.getMouseY();
|
||||
|
||||
rl.setMousePosition(
|
||||
@mod(mouse_x, @as(i32, @intFromFloat(self.window_size.x))),
|
||||
@mod(mouse_y, @as(i32, @intFromFloat(self.window_size.y)))
|
||||
);
|
||||
}
|
||||
|
||||
self.frame_index += 1;
|
||||
_ = self.frameArena().reset(.retain_capacity);
|
||||
self.parent_index_stack.len = 0;
|
||||
|
||||
if (self.active_box_keys.count() == 0) {
|
||||
self.hot_box_key = null;
|
||||
}
|
||||
|
||||
self.events.len = 0;
|
||||
self.mouse = rl.getMousePosition();
|
||||
self.window_size = Vec2.init(@floatFromInt(window_width), @floatFromInt(window_width));
|
||||
self.mouse_buttons = .{};
|
||||
inline for (std.meta.fields(rl.MouseButton)) |mouse_button_field| {
|
||||
const mouse_button: rl.MouseButton = @enumFromInt(mouse_button_field.value);
|
||||
|
||||
if (rl.isMouseButtonPressed(mouse_button)) {
|
||||
self.events.appendAssumeCapacity(Event{ .mouse_pressed = mouse_button });
|
||||
}
|
||||
if (rl.isMouseButtonReleased(mouse_button)) {
|
||||
self.events.appendAssumeCapacity(Event{ .mouse_released = mouse_button });
|
||||
}
|
||||
if (rl.isMouseButtonDown(mouse_button)) {
|
||||
self.mouse_buttons.insert(mouse_button);
|
||||
}
|
||||
}
|
||||
|
||||
const root_box = self.newBox(root_box_key);
|
||||
root_box.size.x = Size.pixels(@floatFromInt(window_width), 1);
|
||||
root_box.size.y = Size.pixels(@floatFromInt(window_height), 1);
|
||||
|
||||
self.pushParent(root_box);
|
||||
}
|
||||
|
||||
pub fn end(self: *UI) void {
|
||||
self.popParent();
|
||||
assert(self.parent_index_stack.len == 0);
|
||||
|
||||
{
|
||||
var i: usize = 0;
|
||||
while (i < self.boxes.len) {
|
||||
const box = &self.boxes.buffer[i];
|
||||
if (box.last_used_frame != self.frame_index) {
|
||||
_ = self.boxes.swapRemove(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var active_box_flags = self.getActiveBoxFlags();
|
||||
var hover_box_flags: Box.Flags = .{};
|
||||
if (self.hot_box_key) |hot_box_key| {
|
||||
if (self.findBoxByKey(hot_box_key)) |hot_box| {
|
||||
hover_box_flags = hot_box.flags;
|
||||
}
|
||||
}
|
||||
|
||||
if (active_box_flags.contains(.draggable)) {
|
||||
rl.setMouseCursor(@intFromEnum(rl.MouseCursor.mouse_cursor_resize_ew));
|
||||
} else if (hover_box_flags.contains(.clickable)) {
|
||||
rl.setMouseCursor(@intFromEnum(rl.MouseCursor.mouse_cursor_pointing_hand));
|
||||
} else {
|
||||
rl.setMouseCursor(@intFromEnum(rl.MouseCursor.mouse_cursor_default));
|
||||
}
|
||||
}
|
||||
|
||||
const root_box = self.findBoxByKey(root_box_key).?;
|
||||
self.calcLayout(root_box, .X);
|
||||
self.calcLayout(root_box, .Y);
|
||||
}
|
||||
|
||||
fn getActiveBoxFlags(self: *UI) Box.Flags {
|
||||
var result_flags: Box.Flags = .{};
|
||||
|
||||
var active_iter = self.active_box_keys.iterator();
|
||||
while (active_iter.next()) |active_box_key| {
|
||||
if (self.findBoxByKey(active_box_key.value.*)) |active_box| {
|
||||
result_flags = result_flags.unionWith(active_box.flags);
|
||||
}
|
||||
}
|
||||
|
||||
return result_flags;
|
||||
}
|
||||
|
||||
pub fn draw(self: *UI) void {
|
||||
const root_box = self.findBoxByKey(root_box_key).?;
|
||||
self.drawBox(root_box);
|
||||
|
||||
if (debug) {
|
||||
const font = Assets.font(.text);
|
||||
const debug_box = Rect{
|
||||
.x = self.mouse.x,
|
||||
.y = self.mouse.y + 32,
|
||||
.width = 400,
|
||||
.height = 100
|
||||
};
|
||||
rl.drawRectangleRec(debug_box, srcery.hard_black);
|
||||
|
||||
var layout_y: f32 = 0;
|
||||
|
||||
{
|
||||
var buff: [256]u8 = undefined;
|
||||
const text = std.fmt.bufPrint(&buff, "Hot: {?}", .{self.hot_box_key}) catch "<Not enough space>";
|
||||
font.drawText(text, .{ .x = debug_box.x, .y = debug_box.y + layout_y }, srcery.white);
|
||||
layout_y += 16;
|
||||
}
|
||||
|
||||
{
|
||||
var rect: ?Rect = null;
|
||||
if (self.hot_box_key) |hot_box_key| {
|
||||
rect = self.findBoxByKey(hot_box_key).?.computedRect();
|
||||
}
|
||||
|
||||
var buff: [256]u8 = undefined;
|
||||
const text = std.fmt.bufPrint(&buff, "Hot rect: {?}", .{RectFormatted.init(rect)}) catch "<Not enough space>";
|
||||
font.drawText(text, .{ .x = debug_box.x, .y = debug_box.y + layout_y }, srcery.white);
|
||||
layout_y += 16;
|
||||
}
|
||||
|
||||
{
|
||||
var hot_parent_key: ?Key = null;
|
||||
if (self.hot_box_key) |hot_box_key| {
|
||||
if (self.findBoxByKey(hot_box_key)) |hot_box| {
|
||||
if (hot_box.parent_index) |parent_index| {
|
||||
hot_parent_key = self.boxes.buffer[parent_index].key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var buff: [256]u8 = undefined;
|
||||
const text = std.fmt.bufPrint(&buff, "Parent of hot: {?}", .{hot_parent_key}) catch "<Not enough space>";
|
||||
font.drawText(text, .{ .x = debug_box.x, .y = debug_box.y + layout_y }, srcery.white);
|
||||
layout_y += 16;
|
||||
}
|
||||
|
||||
// {
|
||||
// var buff: [256]u8 = undefined;
|
||||
// const text = std.fmt.bufPrint(&buff, "Active: {?}", .{self.active_box_key}) catch "<Not enough space>";
|
||||
// font.drawText(text, .{ .x = debug_box.x, .y = debug_box.y + layout_y }, srcery.white);
|
||||
// layout_y += 16;
|
||||
// }
|
||||
|
||||
{
|
||||
font.drawText("Children of hot:", .{ .x = debug_box.x, .y = debug_box.y + layout_y }, srcery.white);
|
||||
layout_y += 16;
|
||||
|
||||
if (self.hot_box_key) |hot_box_key| {
|
||||
const hot_box = self.findBoxByKey(hot_box_key).?;
|
||||
var child_iter = self.iterChildrenByParent(hot_box);
|
||||
while (child_iter.next()) |child| {
|
||||
var buff: [256]u8 = undefined;
|
||||
const text = std.fmt.bufPrint(&buff, "{}", .{child.key}) catch "<Not enough space>";
|
||||
font.drawText(text, .{ .x = debug_box.x, .y = debug_box.y + layout_y }, srcery.white);
|
||||
layout_y += 16;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn drawBox(self: *UI, box: *Box) void {
|
||||
const box_rect = box.computedRect();
|
||||
|
||||
if (box.background) |background| {
|
||||
rl.drawRectangleRec(box_rect, background);
|
||||
}
|
||||
|
||||
if (self.isBoxActive(box.key)) {
|
||||
rl.drawRectangleLinesEx(box_rect, 2, rl.Color.orange);
|
||||
} else if (self.isBoxHot(box.key)) {
|
||||
rl.drawRectangleLinesEx(box_rect, 3, rl.Color.blue);
|
||||
}
|
||||
|
||||
if (box.texture) |texture| {
|
||||
const source = rl.Rectangle{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.width = @floatFromInt(texture.width),
|
||||
.height = @floatFromInt(texture.height)
|
||||
};
|
||||
rl.drawTexturePro(
|
||||
texture,
|
||||
source,
|
||||
box_rect,
|
||||
rl.Vector2.zero(),
|
||||
0, rl.Color.white
|
||||
);
|
||||
}
|
||||
|
||||
if (box.text) |text| {
|
||||
const font = Assets.font(text.font);
|
||||
font.drawTextCenter(text.content, rect_utils.center(box_rect), text.color);
|
||||
}
|
||||
|
||||
var child_iter = self.iterChildrenByParent(box);
|
||||
while (child_iter.next()) |child| {
|
||||
self.drawBox(child);
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
if (self.isBoxActive(box.key)) {
|
||||
rl.drawRectangleLinesEx(box_rect, 3, rl.Color.red);
|
||||
} else if (self.isBoxHot(box.key)) {
|
||||
rl.drawRectangleLinesEx(box_rect, 3, rl.Color.orange);
|
||||
} else {
|
||||
rl.drawRectangleLinesEx(box_rect, 1, rl.Color.pink);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fn getVec2Axis(vec2: *Vec2, axis: Axis) *f32 {
|
||||
return switch (axis) {
|
||||
.X => &vec2.x,
|
||||
.Y => &vec2.y
|
||||
};
|
||||
}
|
||||
|
||||
fn calcLayout(self: *UI, box: *Box, axis: Axis) void {
|
||||
self.calcLayoutStandaloneSize(box, axis);
|
||||
self.calcLayoutUpwardsSize(box, axis);
|
||||
self.calcLayoutDownardsSize(box, axis);
|
||||
self.calcLayoutEnforceConstraints(box, axis);
|
||||
self.calcLayoutPositions(box, axis);
|
||||
}
|
||||
|
||||
fn calcLayoutStandaloneSize(self: *UI, box: *Box, axis: Axis) void {
|
||||
const size = box.size.getAxis(axis);
|
||||
const computed_size = getVec2Axis(&box.persistent.size, axis);
|
||||
|
||||
if (size.kind == .pixels) {
|
||||
computed_size.* = size.kind.pixels;
|
||||
} else if (size.kind == .text) {
|
||||
if (box.text) |text| {
|
||||
const font = Assets.font(text.font);
|
||||
var text_size = font.measureText(text.content);
|
||||
computed_size.* = getVec2Axis(&text_size, axis).*;
|
||||
computed_size.* += size.kind.text * font.getSize();
|
||||
} else {
|
||||
computed_size.* = 0;
|
||||
}
|
||||
}
|
||||
|
||||
var child_iter = self.iterChildrenByParent(box);
|
||||
while (child_iter.next()) |child| {
|
||||
self.calcLayoutStandaloneSize(child, axis);
|
||||
}
|
||||
}
|
||||
|
||||
fn calcLayoutUpwardsSize(self: *UI, box: *Box, axis: Axis) void {
|
||||
const size = box.size.getAxis(axis);
|
||||
const computed_size = getVec2Axis(&box.persistent.size, axis);
|
||||
|
||||
if (size.kind == .percent) {
|
||||
var maybe_fixed_parent: ?*Box = null;
|
||||
|
||||
var parent_iter = self.iterUpwardByParent(box);
|
||||
while (parent_iter.next()) |parent| {
|
||||
const parent_size_kind = parent.size.getAxis(axis).kind;
|
||||
if (parent_size_kind == .pixels or parent_size_kind == .percent or parent_size_kind == .text) {
|
||||
maybe_fixed_parent = parent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (maybe_fixed_parent) |fixed_parent| {
|
||||
computed_size.* = getVec2Axis(&fixed_parent.persistent.size, axis).* * size.kind.percent;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var child_iter = self.iterChildrenByParent(box);
|
||||
while (child_iter.next()) |child| {
|
||||
self.calcLayoutUpwardsSize(child, axis);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn calcLayoutDownardsSize(self: *UI, box: *Box, axis: Axis) void {
|
||||
{
|
||||
var child_iter = self.iterChildrenByParent(box);
|
||||
while (child_iter.next()) |child| {
|
||||
self.calcLayoutDownardsSize(child, axis);
|
||||
}
|
||||
}
|
||||
|
||||
const size = box.size.getAxis(axis);
|
||||
const computed_size = getVec2Axis(&box.persistent.size, axis);
|
||||
|
||||
if (size.kind == .children_sum) {
|
||||
var sum: f32 = 0;
|
||||
var child_iter = self.iterChildrenByParent(box);
|
||||
while (child_iter.next()) |child| {
|
||||
const child_size = getVec2Axis(&child.persistent.size, axis).*;
|
||||
|
||||
if (box.layout_axis == axis) {
|
||||
sum += child_size;
|
||||
} else {
|
||||
sum = @max(sum, child_size);
|
||||
}
|
||||
}
|
||||
|
||||
computed_size.* = sum;
|
||||
}
|
||||
}
|
||||
|
||||
fn calcLayoutPositions(self: *UI, box: *Box, axis: Axis) void {
|
||||
{
|
||||
var layout_position: f32 = 0;
|
||||
|
||||
var child_iter = self.iterChildrenByParent(box);
|
||||
while (child_iter.next()) |child| {
|
||||
const child_axis_position = getVec2Axis(&child.persistent.position, axis);
|
||||
const child_axis_size = getVec2Axis(&child.persistent.size, axis);
|
||||
const parent_axis_position = getVec2Axis(&box.persistent.position, axis);
|
||||
const child_override_position = switch (axis) {
|
||||
.X => child.override_x,
|
||||
.Y => child.override_y,
|
||||
};
|
||||
|
||||
child_axis_position.* = parent_axis_position.*;
|
||||
|
||||
if (child_override_position) |position| {
|
||||
child_axis_position.* += position;
|
||||
} else if (box.layout_axis == axis) {
|
||||
child_axis_position.* += layout_position;
|
||||
layout_position += child_axis_size.*;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var child_iter = self.iterChildrenByParent(box);
|
||||
while (child_iter.next()) |child| {
|
||||
self.calcLayoutPositions(child, axis);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn calcLayoutEnforceConstraints(self: *UI, box: *Box, axis: Axis) void {
|
||||
// Children can't be wider than the parent on the secondary axis
|
||||
if (box.layout_axis != axis) {
|
||||
const max_child_size = getVec2Axis(&box.persistent.size, axis).*;
|
||||
|
||||
var child_iter = self.iterChildrenByParent(box);
|
||||
while (child_iter.next()) |child| {
|
||||
const child_size = getVec2Axis(&child.persistent.size, axis);
|
||||
|
||||
if (child_size.* > max_child_size) {
|
||||
child_size.* = max_child_size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Children need to be shrunk relative to "strictness" on the primary axis
|
||||
if (box.layout_axis == axis) {
|
||||
const max_sum_children_size = getVec2Axis(&box.persistent.size, axis).*;
|
||||
var sum_children_size: f32 = 0;
|
||||
|
||||
var children_fixups: std.BoundedArray(f32, max_boxes) = .{};
|
||||
var children_fixup_sum: f32 = 0;
|
||||
|
||||
{
|
||||
var child_iter = self.iterChildrenByParent(box);
|
||||
while (child_iter.next()) |child| {
|
||||
const child_semantic_size_axis = child.size.getAxis(axis).*;
|
||||
const child_size_axis = getVec2Axis(&child.persistent.size, axis).*;
|
||||
|
||||
sum_children_size += child_size_axis;
|
||||
|
||||
const child_fixup = child_size_axis * (1 - child_semantic_size_axis.strictness);
|
||||
children_fixups.appendAssumeCapacity(child_fixup);
|
||||
children_fixup_sum += child_fixup;
|
||||
}
|
||||
}
|
||||
|
||||
const overflow = sum_children_size - max_sum_children_size;
|
||||
if (overflow > 0) {
|
||||
const overflow_percent = std.math.clamp(overflow / children_fixup_sum, 0, 1);
|
||||
|
||||
var index: usize = 0;
|
||||
var child_iter = self.iterChildrenByParent(box);
|
||||
while (child_iter.next()) |child| : (index += 1) {
|
||||
const child_size_axis = getVec2Axis(&child.persistent.size, axis);
|
||||
|
||||
child_size_axis.* -= children_fixups.buffer[index] * overflow_percent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
{
|
||||
var child_iter = self.iterChildrenByParent(box);
|
||||
while (child_iter.next()) |child| {
|
||||
self.calcLayoutEnforceConstraints(child, axis);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn newBoxFromString(self: *UI, text: []const u8) *Box {
|
||||
var parent_hash: u64 = 0;
|
||||
if (self.getParent()) |parent| {
|
||||
parent_hash = parent.key.hash;
|
||||
}
|
||||
return self.newBox(Key.initString(parent_hash, text));
|
||||
}
|
||||
|
||||
pub fn newBoxFromPtr(self: *UI, ptr: anytype) *Box {
|
||||
return self.newBox(Key.initPtr(ptr));
|
||||
}
|
||||
|
||||
pub fn newBox(self: *UI, key: Key) *Box {
|
||||
assert(key.hash != 0);
|
||||
|
||||
var box: *Box = undefined;
|
||||
var box_index: BoxIndex = undefined;
|
||||
var persistent: Box.Persistent = .{};
|
||||
if (self.findBoxByKey(key)) |found_box| {
|
||||
assert(found_box.last_used_frame < self.frame_index);
|
||||
|
||||
persistent = found_box.persistent;
|
||||
box = found_box;
|
||||
box_index = found_box.index;
|
||||
} else {
|
||||
box = self.boxes.addOneAssumeCapacity();
|
||||
box_index = self.boxes.len - 1;
|
||||
}
|
||||
|
||||
box.* = Box{
|
||||
.key = key,
|
||||
.allocator = self.frameArena().allocator(),
|
||||
.last_used_frame = self.frame_index,
|
||||
.persistent = persistent,
|
||||
|
||||
.index = box_index
|
||||
};
|
||||
|
||||
if (self.getParent()) |parent| {
|
||||
box.parent_index = parent.index;
|
||||
|
||||
if (parent.last_child_index) |last_child_index| {
|
||||
const last_child = &self.boxes.buffer[last_child_index];
|
||||
|
||||
last_child.next_sibling_index = box.index;
|
||||
parent.last_child_index = box.index;
|
||||
} else {
|
||||
parent.first_child_index = box.index;
|
||||
parent.last_child_index = box.index;
|
||||
}
|
||||
}
|
||||
|
||||
return box;
|
||||
}
|
||||
|
||||
fn findBoxByKey(self: *UI, key: Key) ?*Box {
|
||||
for (self.boxes.slice()) |*box| {
|
||||
if (box.key.eql(key)) {
|
||||
return box;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn signalFromBox(self: *UI, box: *Box) Signal {
|
||||
var result = Signal{};
|
||||
|
||||
const key = box.key;
|
||||
const rect = box.computedRect();
|
||||
const is_mouse_inside = rect_utils.isInsideVec2(rect, self.mouse);
|
||||
const clickable = box.flags.contains(.clickable);
|
||||
const draggable = box.flags.contains(.draggable);
|
||||
|
||||
var event_index: usize = 0;
|
||||
while (event_index < self.events.len) {
|
||||
var taken = false;
|
||||
const event: Event = self.events.buffer[event_index];
|
||||
const is_active = self.isBoxActive(key);
|
||||
|
||||
if (event == .mouse_pressed and clickable and is_mouse_inside) {
|
||||
const mouse_button = event.mouse_pressed;
|
||||
result.insertMousePressed(mouse_button);
|
||||
|
||||
self.active_box_keys.put(mouse_button, key);
|
||||
taken = true;
|
||||
}
|
||||
|
||||
if (event == .mouse_released and clickable and is_active and is_mouse_inside) {
|
||||
const mouse_button = event.mouse_released;
|
||||
result.insertMousePressed(mouse_button);
|
||||
result.insertMouseClicked(mouse_button);
|
||||
|
||||
self.active_box_keys.remove(mouse_button);
|
||||
taken = true;
|
||||
}
|
||||
|
||||
if (event == .mouse_released and clickable and is_active and !is_mouse_inside) {
|
||||
const mouse_button = event.mouse_released;
|
||||
result.insertMousePressed(mouse_button);
|
||||
|
||||
self.hot_box_key = null;
|
||||
self.active_box_keys.remove(mouse_button);
|
||||
taken = true;
|
||||
}
|
||||
|
||||
if (taken) {
|
||||
_ = self.events.swapRemove(event_index);
|
||||
} else {
|
||||
event_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (draggable and self.mouse_delta.equals(Vec2.zero()) == 0) {
|
||||
inline for (.{ rl.MouseButton.mouse_button_left, rl.MouseButton.mouse_button_right }) |mouse_button| {
|
||||
const active_box = self.active_box_keys.get(mouse_button);
|
||||
|
||||
if (active_box != null and active_box.?.eql(key)) {
|
||||
result.insertMouseDragged(mouse_button);
|
||||
result.drag = self.mouse_delta;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (is_mouse_inside and clickable) {
|
||||
if (self.hot_box_key == null) {
|
||||
self.hot_box_key = key;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
fn isBoxHot(self: *UI, key: Key) bool {
|
||||
if (self.hot_box_key) |hot_box_key| {
|
||||
return hot_box_key.eql(key);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
fn isBoxActive(self: *UI, key: Key) bool {
|
||||
inline for (std.meta.fields(rl.MouseButton)) |mouse_button_field| {
|
||||
const mouse_button: rl.MouseButton = @enumFromInt(mouse_button_field.value);
|
||||
|
||||
if (self.active_box_keys.get(mouse_button)) |active_box| {
|
||||
if (active_box.eql(key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn getParent(self: *UI) ?*Box {
|
||||
const parent_stack: []BoxIndex = self.parent_index_stack.slice();
|
||||
|
||||
if (parent_stack.len > 0) {
|
||||
const parent_index = parent_stack[parent_stack.len - 1];
|
||||
return &self.boxes.buffer[parent_index];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pushParent(self: *UI, box: *const Box) void {
|
||||
self.parent_index_stack.appendAssumeCapacity(box.index);
|
||||
}
|
||||
|
||||
pub fn popParent(self: *UI) void {
|
||||
_ = self.parent_index_stack.pop();
|
||||
}
|
||||
|
||||
pub fn iterChildrenByParent(self: *UI, box: *const Box) BoxChildIterator {
|
||||
return BoxChildIterator{
|
||||
.boxes = self.boxes.slice(),
|
||||
.current_child = box.first_child_index
|
||||
};
|
||||
}
|
||||
|
||||
pub fn iterUpwardByParent(self: *UI, box: *const Box) BoxParentIterator {
|
||||
return BoxParentIterator{
|
||||
.boxes = self.boxes.slice(),
|
||||
.current_parent = box.parent_index
|
||||
};
|
||||
}
|
||||
|
||||
pub fn frameArena(self: *UI) *std.heap.ArenaAllocator {
|
||||
return &self.arenas[@mod(self.frame_index, 2)];
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
const std = @import("std");
|
||||
const rl = @import("raylib");
|
||||
const Theme = @import("../theme.zig");
|
||||
const UI = @import("./root.zig");
|
||||
const rectUtils = @import("../rect-utils.zig");
|
||||
const utils = @import("../utils.zig");
|
||||
const SourceLocation = std.builtin.SourceLocation;
|
||||
|
||||
const Id = UI.Id;
|
||||
|
||||
pub const ButtonOptions = struct {
|
||||
box: rl.Rectangle,
|
||||
text: []const u8,
|
||||
font: Theme.FontId = .text,
|
||||
};
|
||||
|
||||
pub fn showButtonId(ui: *UI, id: Id, opts: ButtonOptions) bool {
|
||||
const bg_color = Theme.color_button;
|
||||
const text_color = Theme.color_text;
|
||||
const font = Theme.font(opts.font);
|
||||
|
||||
var clicked = false;
|
||||
|
||||
const is_mouse_inside = ui.isMouseInside(opts.box);
|
||||
if (is_mouse_inside) {
|
||||
ui.hot_widget = id;
|
||||
}
|
||||
|
||||
if (ui.isHot(id) and rl.isMouseButtonPressed(.mouse_button_left)) {
|
||||
ui.active_widget = id;
|
||||
clicked = true;
|
||||
}
|
||||
|
||||
if (ui.isActive(id) and rl.isMouseButtonReleased(.mouse_button_left)) {
|
||||
ui.active_widget = null;
|
||||
ui.hot_widget = null;
|
||||
}
|
||||
|
||||
if (ui.isHot(id) and !is_mouse_inside) {
|
||||
ui.hot_widget = null;
|
||||
}
|
||||
|
||||
const text_size = font.measureText(opts.text);
|
||||
var text_position = rectUtils.aligned(opts.box, .center, .center);
|
||||
text_position.x -= text_size.x/2;
|
||||
text_position.y -= text_size.y/2;
|
||||
|
||||
var color = bg_color.fade(0.5);
|
||||
if (ui.isHot(id)) {
|
||||
color = bg_color;
|
||||
}
|
||||
rl.drawRectangleRec(opts.box, color);
|
||||
rl.drawLineV(
|
||||
rectUtils.bottomLeft(opts.box),
|
||||
rectUtils.bottomRight(opts.box),
|
||||
color
|
||||
);
|
||||
font.drawText(opts.text, text_position, text_color);
|
||||
|
||||
return clicked;
|
||||
}
|
||||
|
||||
pub fn showButton(ui: *UI, comptime src: SourceLocation, opts: ButtonOptions) bool {
|
||||
return showButtonId(ui, Id.init(src), opts);
|
||||
}
|
201
src/ui/root.zig
201
src/ui/root.zig
@ -1,201 +0,0 @@
|
||||
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, aggregate ids)
|
||||
|
||||
const UI = @This();
|
||||
|
||||
const max_stack_depth = 16;
|
||||
const TransformFrame = struct {
|
||||
offset: rl.Vector2,
|
||||
scale: rl.Vector2,
|
||||
};
|
||||
const TransformStack = std.BoundedArray(TransformFrame, max_stack_depth);
|
||||
|
||||
hot_widget: ?Id = null,
|
||||
active_widget: ?Id = null,
|
||||
|
||||
transform_stack: TransformStack,
|
||||
|
||||
pub fn init() UI {
|
||||
var stack = TransformStack.init(0) catch unreachable;
|
||||
stack.appendAssumeCapacity(TransformFrame{
|
||||
.offset = rl.Vector2{ .x = 0, .y = 0 },
|
||||
.scale = rl.Vector2{ .x = 1, .y = 1 },
|
||||
});
|
||||
|
||||
return UI{
|
||||
.transform_stack = stack
|
||||
};
|
||||
}
|
||||
|
||||
pub fn isHot(self: *const UI, id: Id) bool {
|
||||
if (self.hot_widget) |hot_id| {
|
||||
return hot_id.eql(id);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn isActive(self: *const UI, id: Id) bool {
|
||||
if (self.active_widget) |active_id| {
|
||||
return active_id.eql(id);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
fn getTopFrame(self: *UI) *TransformFrame {
|
||||
assert(self.transform_stack.len >= 1);
|
||||
return &self.transform_stack.buffer[self.transform_stack.len-1];
|
||||
}
|
||||
|
||||
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) };
|
||||
}
|
||||
|
||||
pub fn eql(a: Id, b: Id) bool {
|
||||
return a.location == b.location and a.extra == b.extra;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Stack = struct {
|
||||
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;
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue
Block a user