add zooming and panning using mouse in channel view

This commit is contained in:
Rokas Puzonas 2025-03-02 16:02:04 +02:00
parent e3588f6836
commit 09fccb7069
8 changed files with 529 additions and 159 deletions

View File

@ -7,9 +7,10 @@ const Assets = @import("./assets.zig");
const Graph = @import("./graph.zig");
const NIDaq = @import("ni-daq/root.zig");
const rect_utils = @import("./rect-utils.zig");
const remap = @import("./utils.zig").remap;
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 assert = std.debug.assert;
const clamp = std.math.clamp;
@ -102,7 +103,7 @@ task_pool: TaskPool,
shown_window: enum {
channels,
add_from_device
} = .add_from_device,
} = .channels,
shown_modal: ?union(enum) {
no_library_error,
@ -118,6 +119,13 @@ last_hot_channel: ?[:0]const u8 = null,
show_device_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 {
self.* = App{
.allocator = allocator,
@ -401,14 +409,15 @@ fn showChannelViewSlider(self: *App, view_rect: *Graph.ViewOptions, sample_count
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.size.y = UI.Size.pixels(32, 1);
}
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.size.x = UI.Size.pixels(8, 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");
{
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.size.x = UI.Size.pixels(8, 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");
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.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| {
graph_box.texture = texture.texture;
}
@ -579,6 +776,10 @@ fn showChannelView(self: *App, channel_view: *ChannelView) !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"));
defer self.ui.popScrollbar();
scroll_area.layout_axis = .Y;
@ -601,13 +802,22 @@ fn showChannelsWindow(self: *App) !void {
center_box.layout_gap = 32;
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()) {
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");
from_device_button.background = srcery.green;
from_device_button.borders.all(.{ .size = 2, .color = srcery.green });
if (self.ui.signalFromBox(from_device_button).clicked()) {
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 });
}
rows.appendAssumeCapacity(Row{
.name = "Foo",
.value = "bar"
});
self.showLabelRows(rows.constSlice());
}
}
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 device_names = try ni_daq.listDeviceNames();
@ -945,46 +1155,47 @@ fn showAddFromDeviceWindow(self: *App) !void {
fn showToolbar(self: *App) void {
const toolbar = self.ui.newBoxFromString("Toolbar");
toolbar.background = rl.Color.green;
toolbar.background = srcery.black;
toolbar.layout_axis = .X;
toolbar.size = .{
.x = UI.Size.percent(1, 0),
.y = UI.Size.pixels(32, 1),
};
toolbar.borders.bottom = .{
.size = 4,
.color = srcery.hard_black
};
self.ui.pushParent(toolbar);
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");
box.background = rl.Color.red;
const box = self.ui.button(.text, "Start all");
box.size.y = UI.Size.percent(1, 1);
const signal = self.ui.signalFromBox(box);
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");
box.background = rl.Color.lime;
const box = self.ui.button(.text, "Help");
box.size.y = UI.Size.percent(1, 1);
const signal = self.ui.signalFromBox(box);
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");
link.flags.insert(.clickable);
link.flags.insert(.hover_mouse_hand);
link.hot_cursor = .mouse_cursor_pointing_hand;
link.flags.insert(.text_underline);
link.size.x = 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.
// So that on the second pass widgets that depend on sizes from other widgets have settled
if (self.ui.frame_index == 0) {
// 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"
if (self.ui.frame_index == 0 or rl.isWindowResized()) {
try self.updateUI();
}

View File

@ -11,6 +11,7 @@ const clamp = std.math.clamp;
const disable_caching = false;
comptime {
// Just making sure that release build has caching enabled
if (builtin.mode != .Debug) {
assert(disable_caching == false);
}
@ -23,7 +24,49 @@ pub const ViewOptions = struct {
max_value: f64,
left_aligned: bool = true,
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 {
@ -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 {
const size_f32: f32 = @floatFromInt(size);
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);
}
const x = mapSampleX(draw_rect, options, @floatFromInt(from_index));
const y_min = mapSampleY(draw_rect, options, column_min);
const y_max = mapSampleY(draw_rect, options, column_max);
const x = options.mapSampleIndexToX(draw_rect.x, draw_rect.width, @floatFromInt(from_index));
const y_min = options.mapSampleValueToY(draw_rect.y, draw_rect.height, column_min);
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(
mapSamplePointToGraph(draw_rect, options, i, samples[from_index]),
mapSamplePointToGraph(draw_rect, options, i-1, samples[clampIndexUsize(i-1, samples.len-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) },
.{ .x = @floatCast(x), .y = @floatCast(avg) },
.{ .x = @floatCast(x), .y = @floatCast(avg+1) },
options.color
);
} else {
@ -161,31 +169,14 @@ fn drawSamples(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f
);
defer rl.endScissorMode();
{
const from_index = clampIndexUsize(@floor(options.from), samples.len);
const to_index = clampIndexUsize(@ceil(options.to) + 1, samples.len);
const from_index = clampIndexUsize(@floor(options.from), samples.len);
const to_index = clampIndexUsize(@ceil(options.to) + 1, samples.len);
if (to_index - from_index > 0) {
for (from_index..(to_index-1)) |i| {
const from_point = mapSamplePointToGraph(draw_rect, options, @floatFromInt(i), samples[i]);
const to_point = mapSamplePointToGraph(draw_rect, options, @floatFromInt(i + 1), samples[i + 1]);
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);
if (to_index - from_index > 0) {
for (from_index..(to_index-1)) |i| {
const from_point = options.mapSampleVec2(draw_rect, @floatFromInt(i), samples[i]);
const to_point = options.mapSampleVec2(draw_rect, @floatFromInt(i + 1), samples[i + 1]);
rl.drawLineV(from_point, to_point, options.color);
}
}
}

19
src/grayscale.fs Normal file
View 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);
}

View File

@ -157,7 +157,9 @@ pub fn main() !void {
if (builtin.mode == .Debug) {
// 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");
}
@ -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);
}
while (!rl.windowShouldClose()) {
rl.setExitKey(rl.KeyboardKey.key_null);
while (!rl.windowShouldClose() and !app.should_close) {
rl.beginDrawing();
defer rl.endDrawing();

View File

@ -89,7 +89,7 @@ pub fn toggleConsoleWindow() void {
// TODO: Maybe return the file path instead of an opened file handle?
// 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) {
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_w = filename_w_buffer[0..filename_len];
var filename_buffer: [std.fs.max_path_bytes]u8 = undefined;
// 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, .{ });
return try std.fmt.allocPrint(allocator, "{s}", .{std.fs.path.fmtWtf16LeAsUtf8Lossy(filename_w)});
}
pub fn init() void {

View File

@ -10,6 +10,13 @@ pub fn position(rect: rl.Rectangle) rl.Vector2 {
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 {
return rl.Vector2.init(rect.width, rect.height);
}

View File

@ -2,13 +2,15 @@ const std = @import("std");
const rl = @import("raylib");
const Assets = @import("./assets.zig");
const rect_utils = @import("./rect-utils.zig");
const utils = @import("./utils.zig");
const srcery = @import("./srcery.zig");
const FontFace = @import("./font-face.zig");
const builtin = @import("builtin");
const log = std.log.scoped(.ui);
const assert = std.debug.assert;
const Vec2 = rl.Vector2;
const Rect = rl.Rectangle;
pub const Vec2 = rl.Vector2;
pub const Rect = rl.Rectangle;
const clamp = std.math.clamp;
const UI = @This();
@ -43,7 +45,7 @@ const RectFormatted = struct {
}
};
const Axis = enum {
pub const Axis = enum {
X,
Y,
@ -191,7 +193,10 @@ pub const Signal = struct {
flags: std.EnumSet(Flag) = .{},
drag: Vec2 = .{ .x = 0, .y = 0 },
scroll: Vec2 = .{ .x = 0, .y = 0 },
relative_mouse: Vec2 = .{ .x = 0, .y = 0 },
hot: bool = false,
active: bool = false,
shift_modifier: bool = false,
pub fn clicked(self: Signal) bool {
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);
pub const Style = struct {
borders: Box.Borders = .{},
rounded: bool = false,
background: ?rl.Color = null,
};
pub const Box = struct {
pub const Persistent = struct {
size: Vec2 = .{ .x = 0, .y = 0 },
position: 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 {
@ -255,9 +268,7 @@ pub const Box = struct {
highlight_hot,
highlight_active,
draggable_x,
draggable_y,
draggable,
scrollable,
fixed_x,
@ -265,8 +276,6 @@ pub const Box = struct {
fixed_width,
fixed_height,
hover_mouse_hand,
skip_draw,
text_underline,
@ -276,6 +285,25 @@ pub const Box = struct {
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,
key: Key,
@ -294,6 +322,9 @@ pub const Box = struct {
} = null,
texture: ?rl.Texture2D = null,
view_offset: Vec2 = .{ .x = 0, .y = 0 },
borders: Borders = .{},
hot_cursor: ?rl.MouseCursor = null,
active_cursor: ?rl.MouseCursor = null,
persistent: Persistent = .{},
@ -463,6 +494,8 @@ arenas: [2]std.heap.ArenaAllocator,
boxes: std.BoundedArray(Box, max_boxes) = .{},
parent_index_stack: std.BoundedArray(BoxIndex, max_boxes) = .{},
style_stack: std.BoundedArray(Style, 16) = .{},
style: Style = .{},
frame_index: u64 = 0,
hot_box_key: ?Key = null,
@ -474,16 +507,26 @@ mouse_delta: Vec2 = .{ .x = 0, .y = 0 },
mouse_buttons: std.EnumSet(rl.MouseButton) = .{},
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 {
const shader = rl.loadShaderFromMemory(null, @embedFile("./grayscale.fs"));
assert(shader.id != 0);
return UI{
.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 {
self.arenas[0].deinit();
self.arenas[1].deinit();
rl.unloadShader(self.grayscale_shader);
}
pub fn begin(self: *UI) void {
@ -492,6 +535,7 @@ pub fn begin(self: *UI) void {
const mouse = rl.getMousePosition();
self.mouse_delta = mouse.subtract(self.mouse);
self.dt = rl.getFrameTime();
// TODO: Maybe add a flag to enable this for active box
// const active_box_flags = self.getActiveBoxFlags();
@ -517,13 +561,13 @@ pub fn begin(self: *UI) void {
} else {
i += 1;
}
}
}
self.frame_index += 1;
_ = self.frameArena().reset(.retain_capacity);
self.parent_index_stack.len = 0;
self.style_stack.len = 0;
if (self.active_box_keys.count() == 0) {
self.hot_box_key = null;
@ -563,24 +607,50 @@ pub fn end(self: *UI) void {
self.popParent();
assert(self.parent_index_stack.len == 0);
// Mouse cursor
{
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;
var cursor: ?rl.MouseCursor = null;
var active_iter = self.active_box_keys.iterator();
while (active_iter.next()) |active_box_key| {
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)) {
rl.setMouseCursor(@intFromEnum(rl.MouseCursor.mouse_cursor_resize_ew));
} else if (active_box_flags.contains(.draggable_y)) {
rl.setMouseCursor(@intFromEnum(rl.MouseCursor.mouse_cursor_resize_ns));
} 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));
if (cursor == null) {
if (self.hot_box_key) |hot_box_key| {
if (self.findBoxByKey(hot_box_key)) |hot_box| {
cursor = hot_box.hot_cursor;
}
}
}
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).?;
@ -588,6 +658,14 @@ pub fn end(self: *UI) void {
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 {
var result_flags: Box.Flags = .{};
@ -603,7 +681,15 @@ fn getActiveBoxFlags(self: *UI) Box.Flags {
pub fn draw(self: *UI) void {
const root_box = self.findBoxByKey(root_box_key).?;
self.drawBox(root_box);
if (self.show_grayscale) {
self.grayscale_shader.activate();
defer self.grayscale_shader.deactivate();
self.drawBox(root_box);
} else {
self.drawBox(root_box);
}
if (debug) {
const font = Assets.font(.text);
@ -682,6 +768,13 @@ fn drawBox(self: *UI, box: *Box) void {
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 do_scissor = box.hasClipping();
@ -696,16 +789,29 @@ fn drawBox(self: *UI, box: *Box) void {
defer if (do_scissor) rl.endScissorMode();
if (box.background) |background| {
rl.drawRectangleRec(box_rect, background);
rl.drawRectangleRec(box_rect, utils.shiftColorInHSV(background, value_shift));
}
if (self.isKeyActive(box.key)) {
if (box.flags.contains(.highlight_active)) {
rl.drawRectangleLinesEx(box_rect, 2, rl.Color.orange);
}
} else if (self.isKeyHot(box.key)) {
if (box.flags.contains(.highlight_hot)) {
rl.drawRectangleLinesEx(box_rect, 3, rl.Color.blue);
const borders_with_coords = .{
.{ box.borders.left , rl.Vector2.init(0, 0), rl.Vector2.init(0, 1), rl.Vector2.init( 1, 0) },
.{ 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) },
.{ box.borders.bottom, rl.Vector2.init(0, 1), rl.Vector2.init(1, 1), rl.Vector2.init( 0, -1) }
};
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;
}
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)) {
rl.drawLineV(
rect_utils.bottomLeft(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.?
};
if (!key.isNil()) {
box.background = self.style.background;
box.rounded = self.style.rounded;
box.borders = self.style.borders;
}
return box;
}
@ -1221,7 +1336,7 @@ pub fn signalFromBox(self: *UI, box: *Box) Signal {
const key = box.key;
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 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) {
const mouse_button = event.mouse_released;
result.insertMousePressed(mouse_button);
result.insertMouseReleased(mouse_button);
result.insertMouseClicked(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) {
const mouse_button = event.mouse_released;
result.insertMousePressed(mouse_button);
result.insertMouseReleased(mouse_button);
self.hot_box_key = null;
self.active_box_keys.remove(mouse_button);
@ -1279,7 +1394,6 @@ pub fn signalFromBox(self: *UI, box: *Box) Signal {
result.insertMouseDragged(mouse_button);
result.drag = self.mouse_delta;
}
}
}
@ -1290,6 +1404,9 @@ pub fn signalFromBox(self: *UI, box: *Box) Signal {
}
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;
}
@ -1412,7 +1529,7 @@ pub fn popScrollbar(self: *UI) void {
if (!content_area.flags.contains(.skip_draw)) {
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.size = .{
.x = UI.Size.pixels(24, 1),
@ -1421,10 +1538,13 @@ pub fn popScrollbar(self: *UI) void {
self.pushParent(scrollbar_area);
defer self.popParent();
const draggable = self.newBoxFromString("Scrollbar button");
draggable.background = rl.Color.dark_brown;
draggable.flags.insert(.clickable);
draggable.flags.insert(.draggable_y);
const draggable = self.clickableBox("Scrollbar button");
draggable.background = srcery.black;
draggable.borders.all(.{
.size = 4,
.color = srcery.xgray3
});
draggable.flags.insert(.draggable);
draggable.size = .{
.x = UI.Size.percent(1, 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(.highlight_active);
box.flags.insert(.highlight_hot);
box.flags.insert(.hover_mouse_hand);
box.hot_cursor = rl.MouseCursor.mouse_cursor_pointing_hand;
return box;
}

View File

@ -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);
}
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 {
rl.drawRectangleRec(rl.Rectangle{
.x = rect.x,