add zooming and panning using mouse in channel view
This commit is contained in:
parent
e3588f6836
commit
09fccb7069
285
src/app.zig
285
src/app.zig
@ -7,9 +7,10 @@ const Assets = @import("./assets.zig");
|
|||||||
const Graph = @import("./graph.zig");
|
const Graph = @import("./graph.zig");
|
||||||
const NIDaq = @import("ni-daq/root.zig");
|
const NIDaq = @import("ni-daq/root.zig");
|
||||||
const rect_utils = @import("./rect-utils.zig");
|
const rect_utils = @import("./rect-utils.zig");
|
||||||
const remap = @import("./utils.zig").remap;
|
|
||||||
const TaskPool = @import("ni-daq/task-pool.zig");
|
const TaskPool = @import("ni-daq/task-pool.zig");
|
||||||
|
|
||||||
|
const remap = @import("./utils.zig").remap;
|
||||||
|
const lerpColor = @import("./utils.zig").lerpColor;
|
||||||
const log = std.log.scoped(.app);
|
const log = std.log.scoped(.app);
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const clamp = std.math.clamp;
|
const clamp = std.math.clamp;
|
||||||
@ -102,7 +103,7 @@ task_pool: TaskPool,
|
|||||||
shown_window: enum {
|
shown_window: enum {
|
||||||
channels,
|
channels,
|
||||||
add_from_device
|
add_from_device
|
||||||
} = .add_from_device,
|
} = .channels,
|
||||||
|
|
||||||
shown_modal: ?union(enum) {
|
shown_modal: ?union(enum) {
|
||||||
no_library_error,
|
no_library_error,
|
||||||
@ -118,6 +119,13 @@ last_hot_channel: ?[:0]const u8 = null,
|
|||||||
show_device_filter_dropdown: bool = false,
|
show_device_filter_dropdown: bool = false,
|
||||||
show_channel_type_filter_dropdown: bool = false,
|
show_channel_type_filter_dropdown: bool = false,
|
||||||
|
|
||||||
|
should_close: bool = false,
|
||||||
|
|
||||||
|
graph_start_sample: ?struct {
|
||||||
|
value: f64,
|
||||||
|
axis: UI.Axis
|
||||||
|
} = null,
|
||||||
|
|
||||||
pub fn init(self: *App, allocator: std.mem.Allocator) !void {
|
pub fn init(self: *App, allocator: std.mem.Allocator) !void {
|
||||||
self.* = App{
|
self.* = App{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
@ -401,14 +409,15 @@ fn showChannelViewSlider(self: *App, view_rect: *Graph.ViewOptions, sample_count
|
|||||||
|
|
||||||
const middle_box = self.ui.clickableBox("Middle knob");
|
const middle_box = self.ui.clickableBox("Middle knob");
|
||||||
{
|
{
|
||||||
middle_box.flags.insert(.draggable_x);
|
middle_box.flags.insert(.draggable);
|
||||||
middle_box.background = rl.Color.black.alpha(0.5);
|
middle_box.background = rl.Color.black.alpha(0.5);
|
||||||
middle_box.size.y = UI.Size.pixels(32, 1);
|
middle_box.size.y = UI.Size.pixels(32, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const left_knob_box = self.ui.clickableBox("Left knob");
|
const left_knob_box = self.ui.clickableBox("Left knob");
|
||||||
{
|
{
|
||||||
left_knob_box.flags.insert(.draggable_x);
|
left_knob_box.active_cursor = .mouse_cursor_resize_ew;
|
||||||
|
left_knob_box.flags.insert(.draggable);
|
||||||
left_knob_box.background = rl.Color.black.alpha(0.5);
|
left_knob_box.background = rl.Color.black.alpha(0.5);
|
||||||
left_knob_box.size.x = UI.Size.pixels(8, 1);
|
left_knob_box.size.x = UI.Size.pixels(8, 1);
|
||||||
left_knob_box.size.y = UI.Size.pixels(32, 1);
|
left_knob_box.size.y = UI.Size.pixels(32, 1);
|
||||||
@ -416,7 +425,8 @@ fn showChannelViewSlider(self: *App, view_rect: *Graph.ViewOptions, sample_count
|
|||||||
|
|
||||||
const right_knob_box = self.ui.clickableBox("Right knob");
|
const right_knob_box = self.ui.clickableBox("Right knob");
|
||||||
{
|
{
|
||||||
right_knob_box.flags.insert(.draggable_x);
|
right_knob_box.active_cursor = .mouse_cursor_resize_ew;
|
||||||
|
right_knob_box.flags.insert(.draggable);
|
||||||
right_knob_box.background = rl.Color.black.alpha(0.5);
|
right_knob_box.background = rl.Color.black.alpha(0.5);
|
||||||
right_knob_box.size.x = UI.Size.pixels(8, 1);
|
right_knob_box.size.x = UI.Size.pixels(8, 1);
|
||||||
right_knob_box.size.y = UI.Size.pixels(32, 1);
|
right_knob_box.size.y = UI.Size.pixels(32, 1);
|
||||||
@ -561,12 +571,199 @@ fn showChannelView(self: *App, channel_view: *ChannelView) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
var channel_rect_opts: *Graph.ViewOptions = &channel_view.view_rect;
|
||||||
|
|
||||||
const graph_box = self.ui.newBoxFromString("Graph");
|
const graph_box = self.ui.newBoxFromString("Graph");
|
||||||
graph_box.background = rl.Color.blue;
|
graph_box.flags.insert(.clickable);
|
||||||
|
graph_box.flags.insert(.draggable);
|
||||||
|
graph_box.background = srcery.black;
|
||||||
graph_box.size.x = UI.Size.percent(1, 0);
|
graph_box.size.x = UI.Size.percent(1, 0);
|
||||||
graph_box.size.y = UI.Size.pixels(channel_view.height, 1);
|
graph_box.size.y = UI.Size.pixels(channel_view.height, 1);
|
||||||
|
self.ui.pushParent(graph_box);
|
||||||
|
defer self.ui.popParent();
|
||||||
|
|
||||||
Graph.drawCached(&channel_view.view_cache, graph_box.persistent.size, channel_view.view_rect, samples);
|
const graph_rect = graph_box.computedRect();
|
||||||
|
|
||||||
|
const signal = self.ui.signalFromBox(graph_box);
|
||||||
|
|
||||||
|
var axis = UI.Axis.X;
|
||||||
|
var zooming: bool = false;
|
||||||
|
var start_sample: ?f64 = null;
|
||||||
|
var stop_sample: ?f64 = null;
|
||||||
|
|
||||||
|
if (signal.hot) {
|
||||||
|
if (signal.shift_modifier) {
|
||||||
|
axis = UI.Axis.Y;
|
||||||
|
} else {
|
||||||
|
axis = UI.Axis.X;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.graph_start_sample) |graph_start_sample| {
|
||||||
|
axis = graph_start_sample.axis;
|
||||||
|
zooming = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mouse_sample: f64 = undefined;
|
||||||
|
if (axis == .X) {
|
||||||
|
const mouse_sample_index = channel_rect_opts.mapSampleXToIndex(0, graph_rect.width, signal.relative_mouse.x);
|
||||||
|
mouse_sample = mouse_sample_index;
|
||||||
|
} else if (axis == .Y) {
|
||||||
|
const mouse_sample_value = channel_rect_opts.mapSampleYToValue(0, graph_rect.height, signal.relative_mouse.y);
|
||||||
|
mouse_sample = mouse_sample_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
start_sample = mouse_sample;
|
||||||
|
|
||||||
|
if (signal.flags.contains(.right_pressed)) {
|
||||||
|
self.graph_start_sample = .{
|
||||||
|
.value = mouse_sample,
|
||||||
|
.axis = axis
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.graph_start_sample) |graph_start_sample| {
|
||||||
|
start_sample = graph_start_sample.value;
|
||||||
|
stop_sample = mouse_sample;
|
||||||
|
zooming = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zooming) {
|
||||||
|
if (axis == .X) {
|
||||||
|
graph_box.active_cursor = .mouse_cursor_resize_ew;
|
||||||
|
} else {
|
||||||
|
graph_box.active_cursor = .mouse_cursor_resize_ns;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signal.flags.contains(.right_released)) {
|
||||||
|
self.graph_start_sample = null;
|
||||||
|
|
||||||
|
if (start_sample != null and stop_sample != null) {
|
||||||
|
const lower_sample: f64 = @min(start_sample.?, stop_sample.?);
|
||||||
|
const higher_sample: f64 = @max(start_sample.?, stop_sample.?);
|
||||||
|
|
||||||
|
if (axis == .X) {
|
||||||
|
if (higher_sample - lower_sample > 1) {
|
||||||
|
channel_rect_opts.from = @floatCast(lower_sample);
|
||||||
|
channel_rect_opts.to = @floatCast(higher_sample);
|
||||||
|
} else {
|
||||||
|
// TODO: Show error message that selected range is too small
|
||||||
|
}
|
||||||
|
} else if (axis == .Y) {
|
||||||
|
if (higher_sample - lower_sample > 0.01) {
|
||||||
|
channel_rect_opts.min_value = lower_sample;
|
||||||
|
channel_rect_opts.max_value = higher_sample;
|
||||||
|
} else {
|
||||||
|
// TODO: Show error message that selected range is too small
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start_sample = null;
|
||||||
|
stop_sample = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start_sample != null and stop_sample != null) {
|
||||||
|
|
||||||
|
const fill = self.ui.newBox(UI.Key.initNil());
|
||||||
|
fill.background = srcery.green.alpha(0.5);
|
||||||
|
|
||||||
|
if (axis == .X) {
|
||||||
|
const start_x = channel_rect_opts.mapSampleIndexToX(graph_rect.x, graph_rect.width, start_sample.?);
|
||||||
|
const stop_x = channel_rect_opts.mapSampleIndexToX(graph_rect.x, graph_rect.width, stop_sample.?);
|
||||||
|
|
||||||
|
fill.setFixedRect(.{
|
||||||
|
.x = @floatCast(@min(start_x, stop_x)),
|
||||||
|
.y = graph_rect.y,
|
||||||
|
.width = @floatCast(@abs(start_x - stop_x)),
|
||||||
|
.height = graph_rect.height
|
||||||
|
});
|
||||||
|
} else if (axis == .Y) {
|
||||||
|
const start_y = channel_rect_opts.mapSampleValueToY(graph_rect.y, graph_rect.height, start_sample.?);
|
||||||
|
const stop_y = channel_rect_opts.mapSampleValueToY(graph_rect.y, graph_rect.height, stop_sample.?);
|
||||||
|
|
||||||
|
fill.setFixedRect(.{
|
||||||
|
.x = graph_rect.x,
|
||||||
|
.y = @floatCast(@min(start_y, stop_y)),
|
||||||
|
.width = graph_rect.width,
|
||||||
|
.height = @floatCast(@abs(start_y - stop_y)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start_sample) |sample| {
|
||||||
|
const marker = self.ui.newBox(UI.Key.initNil());
|
||||||
|
marker.background = srcery.green;
|
||||||
|
|
||||||
|
if (axis == .X) {
|
||||||
|
const value = samples[@intFromFloat(sample)];
|
||||||
|
marker.setFmtText(.text, "{d:0.2} | {d:0.6}", .{sample, value});
|
||||||
|
marker.setFixedRect(UI.Rect{
|
||||||
|
.x = @floatCast(channel_rect_opts.mapSampleIndexToX(graph_rect.x, graph_rect.width, sample)),
|
||||||
|
.y = graph_rect.y,
|
||||||
|
.width = 1,
|
||||||
|
.height = graph_rect.height
|
||||||
|
});
|
||||||
|
|
||||||
|
} else if (axis == .Y) {
|
||||||
|
marker.setFmtText(.text, "{d:0.2}", .{sample});
|
||||||
|
marker.setFixedRect(UI.Rect{
|
||||||
|
.x = graph_rect.x,
|
||||||
|
.y = @floatCast(channel_rect_opts.mapSampleValueToY(graph_rect.y, graph_rect.height, sample)),
|
||||||
|
.width = graph_rect.width,
|
||||||
|
.height = 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stop_sample) |sample| {
|
||||||
|
const marker = self.ui.newBox(UI.Key.initNil());
|
||||||
|
marker.background = srcery.green;
|
||||||
|
|
||||||
|
marker.setFmtText(.text, "{d:0.2}", .{sample});
|
||||||
|
if (axis == .X) {
|
||||||
|
marker.setFixedRect(.{
|
||||||
|
.x = @floatCast(channel_rect_opts.mapSampleIndexToX(graph_rect.x, graph_rect.width, sample)),
|
||||||
|
.y = graph_rect.y,
|
||||||
|
.width = 1,
|
||||||
|
.height = graph_rect.height
|
||||||
|
});
|
||||||
|
} else if (axis == .Y) {
|
||||||
|
marker.setFixedRect(.{
|
||||||
|
.x = graph_rect.x,
|
||||||
|
.y = @floatCast(channel_rect_opts.mapSampleValueToY(graph_rect.y, graph_rect.height, sample)),
|
||||||
|
.width = graph_rect.width,
|
||||||
|
.height = 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (signal.dragged()) {
|
||||||
|
if (signal.shift_modifier) {
|
||||||
|
const drag_offset = remap(
|
||||||
|
f64,
|
||||||
|
0, graph_rect.height,
|
||||||
|
0, channel_rect_opts.max_value - channel_rect_opts.min_value,
|
||||||
|
signal.drag.y
|
||||||
|
);
|
||||||
|
|
||||||
|
channel_rect_opts.max_value += @floatCast(drag_offset);
|
||||||
|
channel_rect_opts.min_value += @floatCast(drag_offset);
|
||||||
|
} else {
|
||||||
|
const drag_offset = remap(
|
||||||
|
f64,
|
||||||
|
0, graph_rect.width,
|
||||||
|
0, channel_rect_opts.to - channel_rect_opts.from,
|
||||||
|
signal.drag.x
|
||||||
|
);
|
||||||
|
|
||||||
|
channel_rect_opts.from -= @floatCast(drag_offset);
|
||||||
|
channel_rect_opts.to -= @floatCast(drag_offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Graph.drawCached(&channel_view.view_cache, graph_box.persistent.size, channel_rect_opts.*, samples);
|
||||||
if (channel_view.view_cache.texture) |texture| {
|
if (channel_view.view_cache.texture) |texture| {
|
||||||
graph_box.texture = texture.texture;
|
graph_box.texture = texture.texture;
|
||||||
}
|
}
|
||||||
@ -579,6 +776,10 @@ fn showChannelView(self: *App, channel_view: *ChannelView) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn showChannelsWindow(self: *App) !void {
|
fn showChannelsWindow(self: *App) !void {
|
||||||
|
if (rl.isKeyPressed(rl.KeyboardKey.key_escape)) {
|
||||||
|
self.should_close = true;
|
||||||
|
}
|
||||||
|
|
||||||
const scroll_area = self.ui.pushScrollbar(self.ui.newKeyFromString("Channels"));
|
const scroll_area = self.ui.pushScrollbar(self.ui.newKeyFromString("Channels"));
|
||||||
defer self.ui.popScrollbar();
|
defer self.ui.popScrollbar();
|
||||||
scroll_area.layout_axis = .Y;
|
scroll_area.layout_axis = .Y;
|
||||||
@ -601,13 +802,22 @@ fn showChannelsWindow(self: *App) !void {
|
|||||||
center_box.layout_gap = 32;
|
center_box.layout_gap = 32;
|
||||||
|
|
||||||
const from_file_button = self.ui.button(.text, "Add from file");
|
const from_file_button = self.ui.button(.text, "Add from file");
|
||||||
from_file_button.background = srcery.green;
|
from_file_button.borders.all(.{ .size = 2, .color = srcery.green });
|
||||||
if (self.ui.signalFromBox(from_file_button).clicked()) {
|
if (self.ui.signalFromBox(from_file_button).clicked()) {
|
||||||
log.debug("TODO: Not implemented", .{});
|
std.debug.print("{}\n", .{std.fs.max_path_bytes});
|
||||||
|
if (Platform.openFilePicker(self.allocator)) |filename| {
|
||||||
|
defer self.allocator.free(filename);
|
||||||
|
|
||||||
|
// TODO: Handle error
|
||||||
|
self.appendChannelFromFile(filename) catch @panic("Failed to append channel from file");
|
||||||
|
} else |err| {
|
||||||
|
// TODO: Show error message to user;
|
||||||
|
log.err("Failed to pick file: {}", .{ err });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const from_device_button = self.ui.button(.text, "Add from device");
|
const from_device_button = self.ui.button(.text, "Add from device");
|
||||||
from_device_button.background = srcery.green;
|
from_device_button.borders.all(.{ .size = 2, .color = srcery.green });
|
||||||
if (self.ui.signalFromBox(from_device_button).clicked()) {
|
if (self.ui.signalFromBox(from_device_button).clicked()) {
|
||||||
self.shown_window = .add_from_device;
|
self.shown_window = .add_from_device;
|
||||||
}
|
}
|
||||||
@ -691,16 +901,16 @@ fn showChannelInfoPanel(self: *App, hot_channel: ?[:0]const u8) !void {
|
|||||||
log.err("ni_daq.listDeviceAIMeasurementTypes(): {}", .{ e });
|
log.err("ni_daq.listDeviceAIMeasurementTypes(): {}", .{ e });
|
||||||
}
|
}
|
||||||
|
|
||||||
rows.appendAssumeCapacity(Row{
|
|
||||||
.name = "Foo",
|
|
||||||
.value = "bar"
|
|
||||||
});
|
|
||||||
|
|
||||||
self.showLabelRows(rows.constSlice());
|
self.showLabelRows(rows.constSlice());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn showAddFromDeviceWindow(self: *App) !void {
|
fn showAddFromDeviceWindow(self: *App) !void {
|
||||||
|
if (rl.isKeyPressed(rl.KeyboardKey.key_escape)) {
|
||||||
|
self.shown_window = .channels;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const ni_daq = &(self.ni_daq orelse return);
|
const ni_daq = &(self.ni_daq orelse return);
|
||||||
|
|
||||||
const device_names = try ni_daq.listDeviceNames();
|
const device_names = try ni_daq.listDeviceNames();
|
||||||
@ -945,46 +1155,47 @@ fn showAddFromDeviceWindow(self: *App) !void {
|
|||||||
|
|
||||||
fn showToolbar(self: *App) void {
|
fn showToolbar(self: *App) void {
|
||||||
const toolbar = self.ui.newBoxFromString("Toolbar");
|
const toolbar = self.ui.newBoxFromString("Toolbar");
|
||||||
toolbar.background = rl.Color.green;
|
toolbar.background = srcery.black;
|
||||||
toolbar.layout_axis = .X;
|
toolbar.layout_axis = .X;
|
||||||
toolbar.size = .{
|
toolbar.size = .{
|
||||||
.x = UI.Size.percent(1, 0),
|
.x = UI.Size.percent(1, 0),
|
||||||
.y = UI.Size.pixels(32, 1),
|
.y = UI.Size.pixels(32, 1),
|
||||||
};
|
};
|
||||||
|
toolbar.borders.bottom = .{
|
||||||
|
.size = 4,
|
||||||
|
.color = srcery.hard_black
|
||||||
|
};
|
||||||
self.ui.pushParent(toolbar);
|
self.ui.pushParent(toolbar);
|
||||||
defer self.ui.popParent();
|
defer self.ui.popParent();
|
||||||
|
|
||||||
|
self.ui.pushStyle();
|
||||||
|
defer self.ui.popStyle();
|
||||||
|
|
||||||
|
self.ui.style.borders.all(.{
|
||||||
|
.size = 4,
|
||||||
|
.color = srcery.hard_black
|
||||||
|
});
|
||||||
|
self.ui.style.background = srcery.xgray2;
|
||||||
|
|
||||||
{
|
{
|
||||||
const box = self.ui.button(.text, "Add from file");
|
const box = self.ui.button(.text, "Start all");
|
||||||
box.background = rl.Color.red;
|
|
||||||
box.size.y = UI.Size.percent(1, 1);
|
box.size.y = UI.Size.percent(1, 1);
|
||||||
|
|
||||||
const signal = self.ui.signalFromBox(box);
|
const signal = self.ui.signalFromBox(box);
|
||||||
if (signal.clicked()) {
|
if (signal.clicked()) {
|
||||||
if (Platform.openFilePicker()) |file| {
|
|
||||||
defer file.close();
|
|
||||||
|
|
||||||
// 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 });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.ui.spacer(.{ .x = UI.Size.percent(1, 0) });
|
||||||
|
|
||||||
{
|
{
|
||||||
const box = self.ui.button(.text, "Add from device");
|
const box = self.ui.button(.text, "Help");
|
||||||
box.background = rl.Color.lime;
|
|
||||||
box.size.y = UI.Size.percent(1, 1);
|
box.size.y = UI.Size.percent(1, 1);
|
||||||
|
|
||||||
const signal = self.ui.signalFromBox(box);
|
const signal = self.ui.signalFromBox(box);
|
||||||
if (signal.clicked()) {
|
if (signal.clicked()) {
|
||||||
if (self.shown_window == .add_from_device) {
|
|
||||||
self.shown_window = .channels;
|
|
||||||
} else {
|
|
||||||
self.shown_window = .add_from_device;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1014,7 +1225,7 @@ fn showModalNoLibraryError(self: *App) void {
|
|||||||
|
|
||||||
const link = self.ui.newBoxFromString("Link");
|
const link = self.ui.newBoxFromString("Link");
|
||||||
link.flags.insert(.clickable);
|
link.flags.insert(.clickable);
|
||||||
link.flags.insert(.hover_mouse_hand);
|
link.hot_cursor = .mouse_cursor_pointing_hand;
|
||||||
link.flags.insert(.text_underline);
|
link.flags.insert(.text_underline);
|
||||||
link.size.x = UI.Size.text(1, 1);
|
link.size.x = UI.Size.text(1, 1);
|
||||||
link.size.y = UI.Size.text(1, 1);
|
link.size.y = UI.Size.text(1, 1);
|
||||||
@ -1249,9 +1460,9 @@ pub fn tick(self: *App) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// On the first frame, render the UI twice.
|
// On the first frame or when the window resizes, render the UI twice.
|
||||||
// So that on the second pass widgets that depend on sizes from other widgets have settled
|
// So that on the second pass widgets that depend on sizes from other widgets have "settled"
|
||||||
if (self.ui.frame_index == 0) {
|
if (self.ui.frame_index == 0 or rl.isWindowResized()) {
|
||||||
try self.updateUI();
|
try self.updateUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
115
src/graph.zig
115
src/graph.zig
@ -11,6 +11,7 @@ const clamp = std.math.clamp;
|
|||||||
const disable_caching = false;
|
const disable_caching = false;
|
||||||
|
|
||||||
comptime {
|
comptime {
|
||||||
|
// Just making sure that release build has caching enabled
|
||||||
if (builtin.mode != .Debug) {
|
if (builtin.mode != .Debug) {
|
||||||
assert(disable_caching == false);
|
assert(disable_caching == false);
|
||||||
}
|
}
|
||||||
@ -23,7 +24,49 @@ pub const ViewOptions = struct {
|
|||||||
max_value: f64,
|
max_value: f64,
|
||||||
left_aligned: bool = true,
|
left_aligned: bool = true,
|
||||||
color: rl.Color = srcery.red,
|
color: rl.Color = srcery.red,
|
||||||
dot_size: f32 = 2
|
|
||||||
|
pub fn mapSampleIndexToX(self: ViewOptions, to_x: f64, to_width: f64, index: f64) f64 {
|
||||||
|
return remap(
|
||||||
|
f64,
|
||||||
|
self.from, self.to,
|
||||||
|
to_x, to_x + to_width,
|
||||||
|
index
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mapSampleXToIndex(self: ViewOptions, from_x: f64, from_width: f64, x: f64) f64 {
|
||||||
|
return remap(
|
||||||
|
f64,
|
||||||
|
from_x, from_x + from_width,
|
||||||
|
self.from, self.to,
|
||||||
|
x
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mapSampleValueToY(self: ViewOptions, to_y: f64, to_height: f64, sample: f64) f64 {
|
||||||
|
return remap(
|
||||||
|
f64,
|
||||||
|
self.min_value, self.max_value,
|
||||||
|
to_y + to_height, to_y,
|
||||||
|
sample
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mapSampleYToValue(self: ViewOptions, to_y: f64, to_height: f64, y: f64) f64 {
|
||||||
|
return remap(
|
||||||
|
f64,
|
||||||
|
to_y + to_height, to_y,
|
||||||
|
self.min_value, self.max_value,
|
||||||
|
y
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mapSampleVec2(self: ViewOptions, draw_rect: rl.Rectangle, index: f64, sample: f64) Vec2 {
|
||||||
|
return .{
|
||||||
|
.x = @floatCast(self.mapSampleIndexToX(draw_rect.x, draw_rect.width, index)),
|
||||||
|
.y = @floatCast(self.mapSampleValueToY(draw_rect.y, draw_rect.height, sample))
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Cache = struct {
|
pub const Cache = struct {
|
||||||
@ -61,31 +104,6 @@ pub const Cache = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fn mapSampleX(draw_rect: rl.Rectangle, view_rect: ViewOptions, index: f64) f64 {
|
|
||||||
return remap(
|
|
||||||
f64,
|
|
||||||
view_rect.from, view_rect.to,
|
|
||||||
draw_rect.x, draw_rect.x + draw_rect.width,
|
|
||||||
index
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mapSampleY(draw_rect: rl.Rectangle, view_rect: ViewOptions, sample: f64) f64 {
|
|
||||||
return remap(
|
|
||||||
f64,
|
|
||||||
view_rect.min_value, view_rect.max_value,
|
|
||||||
draw_rect.y + draw_rect.height, draw_rect.y,
|
|
||||||
sample
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mapSamplePointToGraph(draw_rect: rl.Rectangle, view_rect: ViewOptions, index: f64, sample: f64) Vec2 {
|
|
||||||
return .{
|
|
||||||
.x = @floatCast(mapSampleX(draw_rect, view_rect, index)),
|
|
||||||
.y = @floatCast(mapSampleY(draw_rect, view_rect, sample))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clampIndex(value: f32, size: usize) f32 {
|
fn clampIndex(value: f32, size: usize) f32 {
|
||||||
const size_f32: f32 = @floatFromInt(size);
|
const size_f32: f32 = @floatFromInt(size);
|
||||||
return clamp(value, 0, size_f32);
|
return clamp(value, 0, size_f32);
|
||||||
@ -123,25 +141,15 @@ fn drawSamples(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f
|
|||||||
column_max = @max(column_max, sample);
|
column_max = @max(column_max, sample);
|
||||||
}
|
}
|
||||||
|
|
||||||
const x = mapSampleX(draw_rect, options, @floatFromInt(from_index));
|
const x = options.mapSampleIndexToX(draw_rect.x, draw_rect.width, @floatFromInt(from_index));
|
||||||
const y_min = mapSampleY(draw_rect, options, column_min);
|
const y_min = options.mapSampleValueToY(draw_rect.y, draw_rect.height, column_min);
|
||||||
const y_max = mapSampleY(draw_rect, options, column_max);
|
const y_max = options.mapSampleValueToY(draw_rect.y, draw_rect.height, column_max);
|
||||||
|
|
||||||
if (column_samples.len == 1) {
|
if (@abs(y_max - y_min) < 1) {
|
||||||
|
const avg = (y_min + y_max) / 2;
|
||||||
rl.drawLineV(
|
rl.drawLineV(
|
||||||
mapSamplePointToGraph(draw_rect, options, i, samples[from_index]),
|
.{ .x = @floatCast(x), .y = @floatCast(avg) },
|
||||||
mapSamplePointToGraph(draw_rect, options, i-1, samples[clampIndexUsize(i-1, samples.len-1)]),
|
.{ .x = @floatCast(x), .y = @floatCast(avg+1) },
|
||||||
options.color
|
|
||||||
);
|
|
||||||
|
|
||||||
rl.drawLineV(
|
|
||||||
mapSamplePointToGraph(draw_rect, options, i, samples[from_index]),
|
|
||||||
mapSamplePointToGraph(draw_rect, options, i+1, samples[clampIndexUsize(i+1, samples.len-1)]),
|
|
||||||
options.color
|
|
||||||
);
|
|
||||||
} else if (@abs(y_max - y_min) < 1) {
|
|
||||||
rl.drawPixelV(
|
|
||||||
.{ .x = @floatCast(x), .y = @floatCast(y_min) },
|
|
||||||
options.color
|
options.color
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -161,34 +169,17 @@ fn drawSamples(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f
|
|||||||
);
|
);
|
||||||
defer rl.endScissorMode();
|
defer rl.endScissorMode();
|
||||||
|
|
||||||
{
|
|
||||||
const from_index = clampIndexUsize(@floor(options.from), samples.len);
|
const from_index = clampIndexUsize(@floor(options.from), samples.len);
|
||||||
const to_index = clampIndexUsize(@ceil(options.to) + 1, samples.len);
|
const to_index = clampIndexUsize(@ceil(options.to) + 1, samples.len);
|
||||||
|
|
||||||
if (to_index - from_index > 0) {
|
if (to_index - from_index > 0) {
|
||||||
for (from_index..(to_index-1)) |i| {
|
for (from_index..(to_index-1)) |i| {
|
||||||
const from_point = mapSamplePointToGraph(draw_rect, options, @floatFromInt(i), samples[i]);
|
const from_point = options.mapSampleVec2(draw_rect, @floatFromInt(i), samples[i]);
|
||||||
const to_point = mapSamplePointToGraph(draw_rect, options, @floatFromInt(i + 1), samples[i + 1]);
|
const to_point = options.mapSampleVec2(draw_rect, @floatFromInt(i + 1), samples[i + 1]);
|
||||||
rl.drawLineV(from_point, to_point, options.color);
|
rl.drawLineV(from_point, to_point, options.color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
const from_index = clampIndexUsize(@ceil(options.from), samples.len);
|
|
||||||
const to_index = clampIndexUsize(@ceil(options.to), samples.len);
|
|
||||||
|
|
||||||
const min_circle_size = 0.5;
|
|
||||||
const max_circle_size = options.dot_size;
|
|
||||||
var circle_size = remap(f32, samples_threshold, 0.2, min_circle_size, max_circle_size, samples_per_column);
|
|
||||||
circle_size = @min(circle_size, max_circle_size);
|
|
||||||
|
|
||||||
for (from_index..to_index) |i| {
|
|
||||||
const center = mapSamplePointToGraph(draw_rect, options, @floatFromInt(i), samples[i]);
|
|
||||||
rl.drawCircleV(center, circle_size, options.color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn drawCached(cache: *Cache, render_size: Vec2, options: ViewOptions, samples: []const f64) void {
|
pub fn drawCached(cache: *Cache, render_size: Vec2, options: ViewOptions, samples: []const f64) void {
|
||||||
|
19
src/grayscale.fs
Normal file
19
src/grayscale.fs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#version 330
|
||||||
|
|
||||||
|
// Input vertex attributes (from vertex shader)
|
||||||
|
in vec2 fragTexCoord;
|
||||||
|
in vec4 fragColor;
|
||||||
|
|
||||||
|
// Input uniform values
|
||||||
|
uniform sampler2D texture0;
|
||||||
|
uniform vec4 colDiffuse;
|
||||||
|
|
||||||
|
// Output fragment color
|
||||||
|
out vec4 finalColor;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
vec4 texelColor = texture(texture0, fragTexCoord)*colDiffuse*fragColor;
|
||||||
|
float luminance = dot(texelColor.rgb, vec3(0.2126, 0.7152, 0.0722));
|
||||||
|
gl_FragColor = vec4(luminance, luminance, luminance, texelColor.a);
|
||||||
|
}
|
@ -157,7 +157,9 @@ pub fn main() !void {
|
|||||||
|
|
||||||
if (builtin.mode == .Debug) {
|
if (builtin.mode == .Debug) {
|
||||||
// try app.appendChannelFromDevice("Dev1/ai0");
|
// try app.appendChannelFromDevice("Dev1/ai0");
|
||||||
// try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin");
|
try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin");
|
||||||
|
try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin");
|
||||||
|
// try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin");
|
||||||
// try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin");
|
// try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,7 +172,9 @@ pub fn main() !void {
|
|||||||
profiler = try Profiler.init(allocator, 10 * target_fps, @divFloor(std.time.ns_per_s, target_fps), font_face);
|
profiler = try Profiler.init(allocator, 10 * target_fps, @divFloor(std.time.ns_per_s, target_fps), font_face);
|
||||||
}
|
}
|
||||||
|
|
||||||
while (!rl.windowShouldClose()) {
|
rl.setExitKey(rl.KeyboardKey.key_null);
|
||||||
|
|
||||||
|
while (!rl.windowShouldClose() and !app.should_close) {
|
||||||
rl.beginDrawing();
|
rl.beginDrawing();
|
||||||
defer rl.endDrawing();
|
defer rl.endDrawing();
|
||||||
|
|
||||||
|
@ -89,7 +89,7 @@ pub fn toggleConsoleWindow() void {
|
|||||||
|
|
||||||
// TODO: Maybe return the file path instead of an opened file handle?
|
// TODO: Maybe return the file path instead of an opened file handle?
|
||||||
// So the user of this function could do something more interesting.
|
// So the user of this function could do something more interesting.
|
||||||
pub fn openFilePicker() !std.fs.File {
|
pub fn openFilePicker(allocator: std.mem.Allocator) ![]u8 {
|
||||||
if (builtin.os.tag != .windows) {
|
if (builtin.os.tag != .windows) {
|
||||||
return error.NotSupported;
|
return error.NotSupported;
|
||||||
}
|
}
|
||||||
@ -125,13 +125,7 @@ pub fn openFilePicker() !std.fs.File {
|
|||||||
const filename_len = std.mem.indexOfScalar(u16, &filename_w_buffer, 0).?;
|
const filename_len = std.mem.indexOfScalar(u16, &filename_w_buffer, 0).?;
|
||||||
const filename_w = filename_w_buffer[0..filename_len];
|
const filename_w = filename_w_buffer[0..filename_len];
|
||||||
|
|
||||||
var filename_buffer: [std.fs.max_path_bytes]u8 = undefined;
|
return try std.fmt.allocPrint(allocator, "{s}", .{std.fs.path.fmtWtf16LeAsUtf8Lossy(filename_w)});
|
||||||
// It should be safe to do "catch unreachable" here because `filename_buffer` will always be big enough.
|
|
||||||
const filename = std.fmt.bufPrint(&filename_buffer, "{s}", .{std.fs.path.fmtWtf16LeAsUtf8Lossy(filename_w)}) catch unreachable;
|
|
||||||
|
|
||||||
// TODO: Use the `openFileAbsoluteW` function.
|
|
||||||
// Could not get it to work, because it always threw OBJECT_PATH_SYNTAX_BAD error
|
|
||||||
return try std.fs.openFileAbsolute(filename, .{ });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init() void {
|
pub fn init() void {
|
||||||
|
@ -10,6 +10,13 @@ pub fn position(rect: rl.Rectangle) rl.Vector2 {
|
|||||||
return rl.Vector2.init(rect.x, rect.y);
|
return rl.Vector2.init(rect.x, rect.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn positionAt(rect: rl.Rectangle, normalised_position: rl.Vector2) rl.Vector2 {
|
||||||
|
return rl.Vector2.init(
|
||||||
|
rect.x + normalised_position.x * rect.width,
|
||||||
|
rect.y + normalised_position.y * rect.height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn size(rect: rl.Rectangle) rl.Vector2 {
|
pub fn size(rect: rl.Rectangle) rl.Vector2 {
|
||||||
return rl.Vector2.init(rect.width, rect.height);
|
return rl.Vector2.init(rect.width, rect.height);
|
||||||
}
|
}
|
||||||
|
208
src/ui.zig
208
src/ui.zig
@ -2,13 +2,15 @@ const std = @import("std");
|
|||||||
const rl = @import("raylib");
|
const rl = @import("raylib");
|
||||||
const Assets = @import("./assets.zig");
|
const Assets = @import("./assets.zig");
|
||||||
const rect_utils = @import("./rect-utils.zig");
|
const rect_utils = @import("./rect-utils.zig");
|
||||||
|
const utils = @import("./utils.zig");
|
||||||
const srcery = @import("./srcery.zig");
|
const srcery = @import("./srcery.zig");
|
||||||
const FontFace = @import("./font-face.zig");
|
const FontFace = @import("./font-face.zig");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
const log = std.log.scoped(.ui);
|
const log = std.log.scoped(.ui);
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const Vec2 = rl.Vector2;
|
pub const Vec2 = rl.Vector2;
|
||||||
const Rect = rl.Rectangle;
|
pub const Rect = rl.Rectangle;
|
||||||
const clamp = std.math.clamp;
|
const clamp = std.math.clamp;
|
||||||
|
|
||||||
const UI = @This();
|
const UI = @This();
|
||||||
@ -43,7 +45,7 @@ const RectFormatted = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Axis = enum {
|
pub const Axis = enum {
|
||||||
X,
|
X,
|
||||||
Y,
|
Y,
|
||||||
|
|
||||||
@ -191,7 +193,10 @@ pub const Signal = struct {
|
|||||||
flags: std.EnumSet(Flag) = .{},
|
flags: std.EnumSet(Flag) = .{},
|
||||||
drag: Vec2 = .{ .x = 0, .y = 0 },
|
drag: Vec2 = .{ .x = 0, .y = 0 },
|
||||||
scroll: Vec2 = .{ .x = 0, .y = 0 },
|
scroll: Vec2 = .{ .x = 0, .y = 0 },
|
||||||
|
relative_mouse: Vec2 = .{ .x = 0, .y = 0 },
|
||||||
hot: bool = false,
|
hot: bool = false,
|
||||||
|
active: bool = false,
|
||||||
|
shift_modifier: bool = false,
|
||||||
|
|
||||||
pub fn clicked(self: Signal) bool {
|
pub fn clicked(self: Signal) bool {
|
||||||
return self.flags.contains(.left_clicked) or self.flags.contains(.right_clicked);
|
return self.flags.contains(.left_clicked) or self.flags.contains(.right_clicked);
|
||||||
@ -240,13 +245,21 @@ pub const Signal = struct {
|
|||||||
|
|
||||||
const BoxIndex = std.math.IntFittingRange(0, max_boxes);
|
const BoxIndex = std.math.IntFittingRange(0, max_boxes);
|
||||||
|
|
||||||
|
pub const Style = struct {
|
||||||
|
borders: Box.Borders = .{},
|
||||||
|
rounded: bool = false,
|
||||||
|
background: ?rl.Color = null,
|
||||||
|
};
|
||||||
|
|
||||||
pub const Box = struct {
|
pub const Box = struct {
|
||||||
pub const Persistent = struct {
|
pub const Persistent = struct {
|
||||||
size: Vec2 = .{ .x = 0, .y = 0 },
|
size: Vec2 = .{ .x = 0, .y = 0 },
|
||||||
position: Vec2 = .{ .x = 0, .y = 0 },
|
position: Vec2 = .{ .x = 0, .y = 0 },
|
||||||
children_size: Vec2 = .{ .x = 0, .y = 0 },
|
children_size: Vec2 = .{ .x = 0, .y = 0 },
|
||||||
|
|
||||||
sroll_offset: f32 = 0
|
sroll_offset: f32 = 0,
|
||||||
|
hot: f32 = 0,
|
||||||
|
active: f32 = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Flag = enum {
|
pub const Flag = enum {
|
||||||
@ -255,9 +268,7 @@ pub const Box = struct {
|
|||||||
highlight_hot,
|
highlight_hot,
|
||||||
highlight_active,
|
highlight_active,
|
||||||
|
|
||||||
draggable_x,
|
draggable,
|
||||||
draggable_y,
|
|
||||||
|
|
||||||
scrollable,
|
scrollable,
|
||||||
|
|
||||||
fixed_x,
|
fixed_x,
|
||||||
@ -265,8 +276,6 @@ pub const Box = struct {
|
|||||||
fixed_width,
|
fixed_width,
|
||||||
fixed_height,
|
fixed_height,
|
||||||
|
|
||||||
hover_mouse_hand,
|
|
||||||
|
|
||||||
skip_draw,
|
skip_draw,
|
||||||
|
|
||||||
text_underline,
|
text_underline,
|
||||||
@ -276,6 +285,25 @@ pub const Box = struct {
|
|||||||
|
|
||||||
pub const Flags = std.EnumSet(Flag);
|
pub const Flags = std.EnumSet(Flag);
|
||||||
|
|
||||||
|
pub const Border = struct {
|
||||||
|
size: f32 = 0,
|
||||||
|
color: rl.Color = rl.Color.magenta,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Borders = struct {
|
||||||
|
left: Border = .{},
|
||||||
|
right: Border = .{},
|
||||||
|
top: Border = .{},
|
||||||
|
bottom: Border = .{},
|
||||||
|
|
||||||
|
pub fn all(self: *Borders, border: Border) void {
|
||||||
|
self.left = border;
|
||||||
|
self.right = border;
|
||||||
|
self.top = border;
|
||||||
|
self.bottom = border;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
|
|
||||||
key: Key,
|
key: Key,
|
||||||
@ -294,6 +322,9 @@ pub const Box = struct {
|
|||||||
} = null,
|
} = null,
|
||||||
texture: ?rl.Texture2D = null,
|
texture: ?rl.Texture2D = null,
|
||||||
view_offset: Vec2 = .{ .x = 0, .y = 0 },
|
view_offset: Vec2 = .{ .x = 0, .y = 0 },
|
||||||
|
borders: Borders = .{},
|
||||||
|
hot_cursor: ?rl.MouseCursor = null,
|
||||||
|
active_cursor: ?rl.MouseCursor = null,
|
||||||
|
|
||||||
persistent: Persistent = .{},
|
persistent: Persistent = .{},
|
||||||
|
|
||||||
@ -463,6 +494,8 @@ arenas: [2]std.heap.ArenaAllocator,
|
|||||||
|
|
||||||
boxes: std.BoundedArray(Box, max_boxes) = .{},
|
boxes: std.BoundedArray(Box, max_boxes) = .{},
|
||||||
parent_index_stack: std.BoundedArray(BoxIndex, max_boxes) = .{},
|
parent_index_stack: std.BoundedArray(BoxIndex, max_boxes) = .{},
|
||||||
|
style_stack: std.BoundedArray(Style, 16) = .{},
|
||||||
|
style: Style = .{},
|
||||||
|
|
||||||
frame_index: u64 = 0,
|
frame_index: u64 = 0,
|
||||||
hot_box_key: ?Key = null,
|
hot_box_key: ?Key = null,
|
||||||
@ -474,16 +507,26 @@ mouse_delta: Vec2 = .{ .x = 0, .y = 0 },
|
|||||||
mouse_buttons: std.EnumSet(rl.MouseButton) = .{},
|
mouse_buttons: std.EnumSet(rl.MouseButton) = .{},
|
||||||
window_size: Vec2 = .{ .x = 0, .y = 0 },
|
window_size: Vec2 = .{ .x = 0, .y = 0 },
|
||||||
|
|
||||||
|
dt: f32 = 0,
|
||||||
|
|
||||||
|
show_grayscale: bool = false,
|
||||||
|
grayscale_shader: rl.Shader,
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator) UI {
|
pub fn init(allocator: std.mem.Allocator) UI {
|
||||||
|
const shader = rl.loadShaderFromMemory(null, @embedFile("./grayscale.fs"));
|
||||||
|
assert(shader.id != 0);
|
||||||
|
|
||||||
return UI{
|
return UI{
|
||||||
.arenas = .{ std.heap.ArenaAllocator.init(allocator), std.heap.ArenaAllocator.init(allocator) },
|
.arenas = .{ std.heap.ArenaAllocator.init(allocator), std.heap.ArenaAllocator.init(allocator) },
|
||||||
.mouse = rl.getMousePosition()
|
.mouse = rl.getMousePosition(),
|
||||||
|
.grayscale_shader = shader
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *UI) void {
|
pub fn deinit(self: *UI) void {
|
||||||
self.arenas[0].deinit();
|
self.arenas[0].deinit();
|
||||||
self.arenas[1].deinit();
|
self.arenas[1].deinit();
|
||||||
|
rl.unloadShader(self.grayscale_shader);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn begin(self: *UI) void {
|
pub fn begin(self: *UI) void {
|
||||||
@ -492,6 +535,7 @@ pub fn begin(self: *UI) void {
|
|||||||
|
|
||||||
const mouse = rl.getMousePosition();
|
const mouse = rl.getMousePosition();
|
||||||
self.mouse_delta = mouse.subtract(self.mouse);
|
self.mouse_delta = mouse.subtract(self.mouse);
|
||||||
|
self.dt = rl.getFrameTime();
|
||||||
|
|
||||||
// TODO: Maybe add a flag to enable this for active box
|
// TODO: Maybe add a flag to enable this for active box
|
||||||
// const active_box_flags = self.getActiveBoxFlags();
|
// const active_box_flags = self.getActiveBoxFlags();
|
||||||
@ -517,13 +561,13 @@ pub fn begin(self: *UI) void {
|
|||||||
} else {
|
} else {
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.frame_index += 1;
|
self.frame_index += 1;
|
||||||
_ = self.frameArena().reset(.retain_capacity);
|
_ = self.frameArena().reset(.retain_capacity);
|
||||||
self.parent_index_stack.len = 0;
|
self.parent_index_stack.len = 0;
|
||||||
|
self.style_stack.len = 0;
|
||||||
|
|
||||||
if (self.active_box_keys.count() == 0) {
|
if (self.active_box_keys.count() == 0) {
|
||||||
self.hot_box_key = null;
|
self.hot_box_key = null;
|
||||||
@ -563,31 +607,65 @@ pub fn end(self: *UI) void {
|
|||||||
self.popParent();
|
self.popParent();
|
||||||
assert(self.parent_index_stack.len == 0);
|
assert(self.parent_index_stack.len == 0);
|
||||||
|
|
||||||
|
// Mouse cursor
|
||||||
{
|
{
|
||||||
var active_box_flags = self.getActiveBoxFlags();
|
var cursor: ?rl.MouseCursor = null;
|
||||||
var hover_box_flags: Box.Flags = .{};
|
|
||||||
if (self.hot_box_key) |hot_box_key| {
|
var active_iter = self.active_box_keys.iterator();
|
||||||
if (self.findBoxByKey(hot_box_key)) |hot_box| {
|
while (active_iter.next()) |active_box_key| {
|
||||||
hover_box_flags = hot_box.flags;
|
if (self.findBoxByKey(active_box_key.value.*)) |active_box| {
|
||||||
|
cursor = active_box.active_cursor;
|
||||||
|
|
||||||
|
if (cursor != null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (active_box_flags.contains(.draggable_x)) {
|
if (cursor == null) {
|
||||||
rl.setMouseCursor(@intFromEnum(rl.MouseCursor.mouse_cursor_resize_ew));
|
if (self.hot_box_key) |hot_box_key| {
|
||||||
} else if (active_box_flags.contains(.draggable_y)) {
|
if (self.findBoxByKey(hot_box_key)) |hot_box| {
|
||||||
rl.setMouseCursor(@intFromEnum(rl.MouseCursor.mouse_cursor_resize_ns));
|
cursor = hot_box.hot_cursor;
|
||||||
} else if (hover_box_flags.contains(.hover_mouse_hand)) {
|
|
||||||
rl.setMouseCursor(@intFromEnum(rl.MouseCursor.mouse_cursor_pointing_hand));
|
|
||||||
} else {
|
|
||||||
rl.setMouseCursor(@intFromEnum(rl.MouseCursor.mouse_cursor_default));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.setMouseCursor(@intFromEnum(cursor orelse rl.MouseCursor.mouse_cursor_default));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Animations
|
||||||
|
{
|
||||||
|
const fast_rate = 1 - std.math.pow(f32, 2, (-50 * self.dt));
|
||||||
|
|
||||||
|
for (self.boxes.slice()) |*_box| {
|
||||||
|
const box: *Box = _box;
|
||||||
|
if (box.key.isNil()) continue;
|
||||||
|
|
||||||
|
const is_hot: f32 = @floatFromInt(@intFromBool(self.isKeyHot(box.key)));
|
||||||
|
const is_active: f32 = @floatFromInt(@intFromBool(self.isKeyActive(box.key)));
|
||||||
|
|
||||||
|
box.persistent.hot += fast_rate * (is_hot - box.persistent.hot );
|
||||||
|
box.persistent.active += fast_rate * (is_active - box.persistent.active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rl.isKeyPressed(rl.KeyboardKey.key_f5) and builtin.mode == .Debug) {
|
||||||
|
self.show_grayscale = !self.show_grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
const root_box = self.findBoxByKey(root_box_key).?;
|
const root_box = self.findBoxByKey(root_box_key).?;
|
||||||
self.calcLayout(root_box, .X);
|
self.calcLayout(root_box, .X);
|
||||||
self.calcLayout(root_box, .Y);
|
self.calcLayout(root_box, .Y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn pushStyle(self: *UI) void {
|
||||||
|
self.style_stack.appendAssumeCapacity(self.style);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn popStyle(self: *UI) void {
|
||||||
|
self.style = self.style_stack.pop();
|
||||||
|
}
|
||||||
|
|
||||||
fn getActiveBoxFlags(self: *UI) Box.Flags {
|
fn getActiveBoxFlags(self: *UI) Box.Flags {
|
||||||
var result_flags: Box.Flags = .{};
|
var result_flags: Box.Flags = .{};
|
||||||
|
|
||||||
@ -603,7 +681,15 @@ fn getActiveBoxFlags(self: *UI) Box.Flags {
|
|||||||
|
|
||||||
pub fn draw(self: *UI) void {
|
pub fn draw(self: *UI) void {
|
||||||
const root_box = self.findBoxByKey(root_box_key).?;
|
const root_box = self.findBoxByKey(root_box_key).?;
|
||||||
|
|
||||||
|
if (self.show_grayscale) {
|
||||||
|
self.grayscale_shader.activate();
|
||||||
|
defer self.grayscale_shader.deactivate();
|
||||||
|
|
||||||
self.drawBox(root_box);
|
self.drawBox(root_box);
|
||||||
|
} else {
|
||||||
|
self.drawBox(root_box);
|
||||||
|
}
|
||||||
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
const font = Assets.font(.text);
|
const font = Assets.font(.text);
|
||||||
@ -682,6 +768,13 @@ fn drawBox(self: *UI, box: *Box) void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var value_shift: f32 = 0;
|
||||||
|
if (box.flags.contains(.highlight_active) and self.isKeyActive(box.key)) {
|
||||||
|
value_shift = -0.5 * box.persistent.active;
|
||||||
|
} else if (box.flags.contains(.highlight_hot) and self.isKeyHot(box.key)) {
|
||||||
|
value_shift = 0.6 * box.persistent.hot;
|
||||||
|
}
|
||||||
|
|
||||||
const box_rect = box.computedRect();
|
const box_rect = box.computedRect();
|
||||||
|
|
||||||
const do_scissor = box.hasClipping();
|
const do_scissor = box.hasClipping();
|
||||||
@ -696,16 +789,29 @@ fn drawBox(self: *UI, box: *Box) void {
|
|||||||
defer if (do_scissor) rl.endScissorMode();
|
defer if (do_scissor) rl.endScissorMode();
|
||||||
|
|
||||||
if (box.background) |background| {
|
if (box.background) |background| {
|
||||||
rl.drawRectangleRec(box_rect, background);
|
rl.drawRectangleRec(box_rect, utils.shiftColorInHSV(background, value_shift));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.isKeyActive(box.key)) {
|
const borders_with_coords = .{
|
||||||
if (box.flags.contains(.highlight_active)) {
|
.{ box.borders.left , rl.Vector2.init(0, 0), rl.Vector2.init(0, 1), rl.Vector2.init( 1, 0) },
|
||||||
rl.drawRectangleLinesEx(box_rect, 2, rl.Color.orange);
|
.{ box.borders.right , rl.Vector2.init(1, 0), rl.Vector2.init(1, 1), rl.Vector2.init(-1, 0) },
|
||||||
}
|
.{ box.borders.top , rl.Vector2.init(0, 0), rl.Vector2.init(1, 0), rl.Vector2.init( 0, 1) },
|
||||||
} else if (self.isKeyHot(box.key)) {
|
.{ box.borders.bottom, rl.Vector2.init(0, 1), rl.Vector2.init(1, 1), rl.Vector2.init( 0, -1) }
|
||||||
if (box.flags.contains(.highlight_hot)) {
|
};
|
||||||
rl.drawRectangleLinesEx(box_rect, 3, rl.Color.blue);
|
inline for (borders_with_coords) |border_with_coords| {
|
||||||
|
const border = border_with_coords[0];
|
||||||
|
const line_from = border_with_coords[1];
|
||||||
|
const line_to = border_with_coords[2];
|
||||||
|
const inset_direction: rl.Vector2 = border_with_coords[3];
|
||||||
|
|
||||||
|
if (border.size > 0) {
|
||||||
|
const inset = inset_direction.multiply(Vec2.init(border.size/2, border.size/2));
|
||||||
|
rl.drawLineEx(
|
||||||
|
rect_utils.positionAt(box_rect, line_from).add(inset),
|
||||||
|
rect_utils.positionAt(box_rect, line_to).add(inset),
|
||||||
|
border.size,
|
||||||
|
utils.shiftColorInHSV(border.color, value_shift)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -750,13 +856,16 @@ fn drawBox(self: *UI, box: *Box) void {
|
|||||||
text_rect.y += (box_rect.height - text_size.y) / 2;
|
text_rect.y += (box_rect.height - text_size.y) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
font.drawText(text.content, .{ .x = text_rect.x, .y = text_rect.y }, text.color);
|
// text_rect.y += box.persistent.active * text_rect.height*0.1;
|
||||||
|
|
||||||
|
const text_color = utils.shiftColorInHSV(text.color, value_shift);
|
||||||
|
font.drawText(text.content, .{ .x = text_rect.x, .y = text_rect.y }, text_color);
|
||||||
|
|
||||||
if (box.flags.contains(.text_underline)) {
|
if (box.flags.contains(.text_underline)) {
|
||||||
rl.drawLineV(
|
rl.drawLineV(
|
||||||
rect_utils.bottomLeft(text_rect),
|
rect_utils.bottomLeft(text_rect),
|
||||||
rect_utils.bottomRight(text_rect),
|
rect_utils.bottomRight(text_rect),
|
||||||
text.color
|
text_color
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1177,6 +1286,12 @@ pub fn newBoxNoAppend(self: *UI, key: Key) *Box {
|
|||||||
.index = box_index.?
|
.index = box_index.?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!key.isNil()) {
|
||||||
|
box.background = self.style.background;
|
||||||
|
box.rounded = self.style.rounded;
|
||||||
|
box.borders = self.style.borders;
|
||||||
|
}
|
||||||
|
|
||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1221,7 +1336,7 @@ pub fn signalFromBox(self: *UI, box: *Box) Signal {
|
|||||||
|
|
||||||
const key = box.key;
|
const key = box.key;
|
||||||
const clickable = box.flags.contains(.clickable);
|
const clickable = box.flags.contains(.clickable);
|
||||||
const draggable = box.flags.contains(.draggable_x) or box.flags.contains(.draggable_y);
|
const draggable = box.flags.contains(.draggable);
|
||||||
const scrollable = box.flags.contains(.scrollable);
|
const scrollable = box.flags.contains(.scrollable);
|
||||||
const is_mouse_inside = rect_utils.isInsideVec2(rect, self.mouse);
|
const is_mouse_inside = rect_utils.isInsideVec2(rect, self.mouse);
|
||||||
|
|
||||||
@ -1241,7 +1356,7 @@ pub fn signalFromBox(self: *UI, box: *Box) Signal {
|
|||||||
|
|
||||||
if (event == .mouse_released and clickable and is_active and is_mouse_inside) {
|
if (event == .mouse_released and clickable and is_active and is_mouse_inside) {
|
||||||
const mouse_button = event.mouse_released;
|
const mouse_button = event.mouse_released;
|
||||||
result.insertMousePressed(mouse_button);
|
result.insertMouseReleased(mouse_button);
|
||||||
result.insertMouseClicked(mouse_button);
|
result.insertMouseClicked(mouse_button);
|
||||||
|
|
||||||
self.active_box_keys.remove(mouse_button);
|
self.active_box_keys.remove(mouse_button);
|
||||||
@ -1250,7 +1365,7 @@ pub fn signalFromBox(self: *UI, box: *Box) Signal {
|
|||||||
|
|
||||||
if (event == .mouse_released and clickable and is_active and !is_mouse_inside) {
|
if (event == .mouse_released and clickable and is_active and !is_mouse_inside) {
|
||||||
const mouse_button = event.mouse_released;
|
const mouse_button = event.mouse_released;
|
||||||
result.insertMousePressed(mouse_button);
|
result.insertMouseReleased(mouse_button);
|
||||||
|
|
||||||
self.hot_box_key = null;
|
self.hot_box_key = null;
|
||||||
self.active_box_keys.remove(mouse_button);
|
self.active_box_keys.remove(mouse_button);
|
||||||
@ -1279,7 +1394,6 @@ pub fn signalFromBox(self: *UI, box: *Box) Signal {
|
|||||||
result.insertMouseDragged(mouse_button);
|
result.insertMouseDragged(mouse_button);
|
||||||
result.drag = self.mouse_delta;
|
result.drag = self.mouse_delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1290,6 +1404,9 @@ pub fn signalFromBox(self: *UI, box: *Box) Signal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
result.hot = self.isKeyHot(box.key);
|
result.hot = self.isKeyHot(box.key);
|
||||||
|
result.active = self.isKeyActive(box.key);
|
||||||
|
result.relative_mouse = self.mouse.subtract(rect_utils.position(rect));
|
||||||
|
result.shift_modifier = rl.isKeyDown(rl.KeyboardKey.key_left_shift) or rl.isKeyDown(rl.KeyboardKey.key_right_shift);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -1412,7 +1529,7 @@ pub fn popScrollbar(self: *UI) void {
|
|||||||
|
|
||||||
if (!content_area.flags.contains(.skip_draw)) {
|
if (!content_area.flags.contains(.skip_draw)) {
|
||||||
const scrollbar_area = self.newBoxFromString("Scrollbar area");
|
const scrollbar_area = self.newBoxFromString("Scrollbar area");
|
||||||
scrollbar_area.background = rl.Color.gold;
|
scrollbar_area.background = srcery.hard_black;
|
||||||
scrollbar_area.flags.insert(.scrollable);
|
scrollbar_area.flags.insert(.scrollable);
|
||||||
scrollbar_area.size = .{
|
scrollbar_area.size = .{
|
||||||
.x = UI.Size.pixels(24, 1),
|
.x = UI.Size.pixels(24, 1),
|
||||||
@ -1421,10 +1538,13 @@ pub fn popScrollbar(self: *UI) void {
|
|||||||
self.pushParent(scrollbar_area);
|
self.pushParent(scrollbar_area);
|
||||||
defer self.popParent();
|
defer self.popParent();
|
||||||
|
|
||||||
const draggable = self.newBoxFromString("Scrollbar button");
|
const draggable = self.clickableBox("Scrollbar button");
|
||||||
draggable.background = rl.Color.dark_brown;
|
draggable.background = srcery.black;
|
||||||
draggable.flags.insert(.clickable);
|
draggable.borders.all(.{
|
||||||
draggable.flags.insert(.draggable_y);
|
.size = 4,
|
||||||
|
.color = srcery.xgray3
|
||||||
|
});
|
||||||
|
draggable.flags.insert(.draggable);
|
||||||
draggable.size = .{
|
draggable.size = .{
|
||||||
.x = UI.Size.percent(1, 1),
|
.x = UI.Size.percent(1, 1),
|
||||||
.y = UI.Size.percent(visible_percent, 1),
|
.y = UI.Size.percent(visible_percent, 1),
|
||||||
@ -1471,7 +1591,7 @@ pub fn clickableBox(self: *UI, key: []const u8) *Box {
|
|||||||
box.flags.insert(.clickable);
|
box.flags.insert(.clickable);
|
||||||
box.flags.insert(.highlight_active);
|
box.flags.insert(.highlight_active);
|
||||||
box.flags.insert(.highlight_hot);
|
box.flags.insert(.highlight_hot);
|
||||||
box.flags.insert(.hover_mouse_hand);
|
box.hot_cursor = rl.MouseCursor.mouse_cursor_pointing_hand;
|
||||||
|
|
||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,30 @@ pub fn rgba(r: u8, g: u8, b: u8, a: f32) rl.Color {
|
|||||||
return rl.Color.init(r, g, b, a * 255);
|
return rl.Color.init(r, g, b, a * 255);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn lerpColor(from: rl.Color, to: rl.Color, t: f32) rl.Color {
|
||||||
|
const r = std.math.lerp(@as(f32, @floatFromInt(from.r)), @as(f32, @floatFromInt(to.r)), t);
|
||||||
|
const g = std.math.lerp(@as(f32, @floatFromInt(from.g)), @as(f32, @floatFromInt(to.g)), t);
|
||||||
|
const b = std.math.lerp(@as(f32, @floatFromInt(from.b)), @as(f32, @floatFromInt(to.b)), t);
|
||||||
|
const a = std.math.lerp(@as(f32, @floatFromInt(from.a)), @as(f32, @floatFromInt(to.a)), t);
|
||||||
|
|
||||||
|
return rl.Color{
|
||||||
|
.r = @intFromFloat(r),
|
||||||
|
.g = @intFromFloat(g),
|
||||||
|
.b = @intFromFloat(b),
|
||||||
|
.a = @intFromFloat(a),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shiftColorInHSV(color: rl.Color, value_shift: f32) rl.Color {
|
||||||
|
if (value_shift == 0) {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hsv = rl.colorToHSV(color);
|
||||||
|
hsv.z = std.math.clamp(hsv.z * (1 + value_shift), 0, 1);
|
||||||
|
return rl.colorFromHSV(hsv.x, hsv.y, hsv.z);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn drawUnderline(rect: rl.Rectangle, size: f32, color: rl.Color) void {
|
pub fn drawUnderline(rect: rl.Rectangle, size: f32, color: rl.Color) void {
|
||||||
rl.drawRectangleRec(rl.Rectangle{
|
rl.drawRectangleRec(rl.Rectangle{
|
||||||
.x = rect.x,
|
.x = rect.x,
|
||||||
|
Loading…
Reference in New Issue
Block a user