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 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();
|
||||
}
|
||||
|
||||
|
125
src/graph.zig
125
src/graph.zig
@ -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
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) {
|
||||
// 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();
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
|
210
src/ui.zig
210
src/ui.zig
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user