diff --git a/build.zig b/build.zig index 15b9400..76576e7 100644 --- a/build.zig +++ b/build.zig @@ -98,6 +98,12 @@ pub fn build(b: *std.Build) !void { const stb_image_lib = buildStbImage(b); const cute_aseprite_lib = buildCuteAseprite(b, raylib_dep); + const generate_tool = b.addExecutable(.{ + .name = "generate", + .root_source_file = b.path("tools/generate.zig"), + .target = target, + }); + const png_to_icon_tool = b.addExecutable(.{ .name = "png-to-icon", .root_source_file = b.path("tools/png-to-icon.zig"), @@ -142,22 +148,38 @@ pub fn build(b: *std.Build) !void { } b.installArtifact(exe); - const run_cmd = b.addRunArtifact(exe); - run_cmd.step.dependOn(b.getInstallStep()); - if (b.args) |args| { - run_cmd.addArgs(args); + + { // Run genration tool + + const run_cmd = b.addRunArtifact(generate_tool); + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("generate", "Run sample generation tool"); + run_step.dependOn(&run_cmd.step); } - const run_step = b.step("run", "Run the program"); - run_step.dependOn(&run_cmd.step); + { // Run main program + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| { + run_cmd.addArgs(args); + } - const unit_tests = b.addTest(.{ - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, - }); + const run_step = b.step("run", "Run the program"); + run_step.dependOn(&run_cmd.step); + } - const run_unit_tests = b.addRunArtifact(unit_tests); - const test_step = b.step("test", "Run unit tests"); - test_step.dependOn(&run_unit_tests.step); + { // Unit tests + const unit_tests = b.addTest(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + const run_unit_tests = b.addRunArtifact(unit_tests); + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_unit_tests.step); + } } diff --git a/src/app.zig b/src/app.zig index 3083f8d..29e905f 100644 --- a/src/app.zig +++ b/src/app.zig @@ -751,7 +751,7 @@ pub fn tick(self: *App) !void { if (view.reference != .channel) continue; if (!view.follow) continue; - const sample_rate = self.project.sample_rate orelse 1; + const sample_rate = self.project.getSampleRate() orelse 1; view.graph_opts.y_range = view.available_y_range; view.graph_opts.x_range.lower = 0; diff --git a/src/main.zig b/src/main.zig index 16607d7..961307f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -103,15 +103,35 @@ pub fn main() !void { if (builtin.mode == .Debug) { _ = try app.addView(.{ - .channel = try app.addChannel("Dev1/ai0") + .file = try app.addFile("./samples-5k.bin") }); + _ = try app.addView(.{ - .channel = try app.addChannel("Dev3/ao0") + .file = try app.addFile("./samples-50k.bin") }); + _ = try app.addView(.{ - .file = try app.addFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin") + .file = try app.addFile("./samples-300k.bin") }); + _ = try app.addView(.{ + .file = try app.addFile("./samples-9m.bin") + }); + + _ = try app.addView(.{ + .file = try app.addFile("./samples-18m.bin") + }); + + // _ = try app.addView(.{ + // .channel = try app.addChannel("Dev1/ai0") + // }); + // _ = try app.addView(.{ + // .channel = try app.addChannel("Dev3/ao0") + // }); + // _ = try app.addView(.{ + // .file = try app.addFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin") + // }); + var cwd_realpath_buff: [std.fs.MAX_PATH_BYTES]u8 = undefined; const cwd_realpath = try std.fs.cwd().realpath(".", &cwd_realpath_buff); @@ -119,6 +139,7 @@ pub fn main() !void { errdefer allocator.free(allocator); app.project.save_location = save_location; + app.project.sample_rate = 5000; // try app.appendChannelFromDevice("Dev1/ai0"); // try app.appendChannelFromDevice("Dev3/ao0"); diff --git a/src/screens/main_screen.zig b/src/screens/main_screen.zig index 8f651f9..3b189ef 100644 --- a/src/screens/main_screen.zig +++ b/src/screens/main_screen.zig @@ -52,6 +52,7 @@ protocol_error_message: ?[]const u8 = null, protocol_graph_cache: Graph.Cache = .{}, preview_samples: std.ArrayListUnmanaged(f64) = .{}, preview_samples_y_range: RangeF64 = RangeF64.init(0, 0), +view_settings: ?Id = null, pub fn init(app: *App) !MainScreen { const allocator = app.allocator; @@ -223,203 +224,129 @@ fn showChannelViewGraph(self: *MainScreen, view_id: Id) *UI.Box { return graph_box; } -fn getLineOnRuler( - self: *MainScreen, - view_id: Id, - ruler: *UI.Box, +const RulerContext = struct { + one_unit: ?f64, + render_range: RangeF64, + available_range: RangeF64, axis: UI.Axis, + rect: rl.Rectangle = .{ .x = 0, .y = 0, .width = 0, .height = 0 }, - along_axis_pos: f64, - cross_axis_pos: f64, - cross_axis_size: f64 -) rl.Rectangle { - const view = self.app.getView(view_id).?; - const shown_size = view.getGraphView(axis).size(); + fn init(axis: UI.Axis, project: *App.Project, view_id: Id) RulerContext { + const view = project.views.get(view_id).?; - const along_axis_size = shown_size / switch (axis) { - .X => ruler.persistent.size.x, - .Y => ruler.persistent.size.y, - }; - - return self.getRectOnRuler( - view_id, - ruler, - axis, - - along_axis_pos, - along_axis_size, - cross_axis_pos, - cross_axis_size - ); -} - -fn getRectOnRuler( - self: *MainScreen, - view_id: Id, - ruler: *UI.Box, - axis: UI.Axis, - - along_axis_pos: f64, - along_axis_size: f64, - cross_axis_pos: f64, - cross_axis_size: f64 -) rl.Rectangle { - assert(0 <= cross_axis_size and cross_axis_size <= 1); - - const rect = ruler.rect(); - const rect_height: f64 = @floatCast(rect.height); - const rect_width: f64 = @floatCast(rect.width); - - const view = self.app.getView(view_id).?; - const view_range = view.getGraphView(axis).*; - - if (axis == .X) { - const width_range = RangeF64.init(0, rect.width); - var result = rl.Rectangle{ - .width = @floatCast(along_axis_size / view_range.size() * rect_width), - .height = @floatCast(rect_height * cross_axis_size), - .x = @floatCast(view_range.remapTo(width_range, along_axis_pos)), - .y = @floatCast(rect_height * cross_axis_pos), + return RulerContext{ + .one_unit = switch (axis) { + .X => project.getSampleRate(), + .Y => 1 + }, + .render_range = view.getGraphView(axis).*, + .available_range = view.getAvailableView(axis), + .axis = axis, }; - - if (result.width < 0) { - result.x += result.width; - result.width *= -1; - } - - return result; - } else { - const height_range = RangeF64.init(0, rect.height); - var result = rl.Rectangle{ - .width = @floatCast(rect_width * cross_axis_size), - .height = @floatCast(along_axis_size / view_range.size() * rect_height), - .x = @floatCast(rect_width * (1 - cross_axis_pos - cross_axis_size)), - .y = @floatCast(view_range.remapTo(height_range, along_axis_pos + along_axis_size)), - }; - - if (result.height < 0) { - result.y += result.height; - result.height *= -1; - } - - return result; - } -} - -fn showRulerTicksRange( - self: *MainScreen, - view_id: Id, - ruler: *UI.Box, - axis: UI.Axis, - - from: f64, - to: f64, - step: f64, - - marker_size: f64 -) void { - var marker = from; - while (marker < to) : (marker += step) { - _ = self.app.ui.createBox(.{ - .background = srcery.yellow, - .float_rect = self.getLineOnRuler(view_id, ruler, axis, marker, 0, marker_size), - .float_relative_to = ruler - }); - } -} - -fn showRulerTicks(self: *MainScreen, view_id: Id, axis: UI.Axis) void { - const view = self.app.getView(view_id).?; - - const view_range = view.getGraphView(axis); - const full_range = view.getAvailableView(axis); - - var ui = &self.app.ui; - const ruler = ui.parentBox().?; - - const ruler_rect = ruler.rect(); - const ruler_rect_size_along_axis = switch (axis) { - .X => ruler_rect.width, - .Y => ruler_rect.height - }; - - if (ruler_rect_size_along_axis == 0) { - return; } - if (view_range.size() == 0) { - return; - } + fn getPoint(self: *RulerContext, along_axis_pos: f64, cross_axis_pos: f64) rl.Vector2 { + const rect_width: f64 = @floatCast(self.rect.width); + const rect_height: f64 = @floatCast(self.rect.height); - if (full_range.size() == 0) { - return; - } + var x: f64 = undefined; + var y: f64 = undefined; - const ideal_pixels_per_division = 150; - var subdivisions: f32 = 20; - subdivisions = 20; - while (true) { - assert(subdivisions > 0); - const step = full_range.size() / subdivisions; - const pixels_per_division = step / view_range.size() * ruler_rect_size_along_axis; - assert(pixels_per_division > 0); - - if (pixels_per_division > ideal_pixels_per_division*2) { - subdivisions *= 2; - } else if (pixels_per_division < ideal_pixels_per_division/2) { - subdivisions /= 2; + if (self.axis == .X) { + x = remap(f64, self.render_range.lower, self.render_range.upper, 0, rect_width, along_axis_pos); + y = cross_axis_pos * rect_height; } else { - break; + assert(self.axis == .Y); + + x = (1 - cross_axis_pos) * rect_width; + y = remap(f64, self.render_range.lower, self.render_range.upper, 0, rect_height, along_axis_pos); + } + + return rl.Vector2{ + .x = self.rect.x + @as(f32, @floatCast(x)), + .y = self.rect.y + @as(f32, @floatCast(y)), + }; + } + + fn getLine(self: *RulerContext, along_axis_pos: f64, cross_axis_size: f64) rl.Rectangle { + const along_axis_size = self.render_range.size() / switch(self.axis) { + .X => @as(f64, @floatCast(self.rect.width)), + .Y => @as(f64, @floatCast(self.rect.height)) + }; + + return self.getRect( + along_axis_pos, + along_axis_size, + 0, + cross_axis_size + ); + } + + fn getRect(self: *RulerContext, along_axis_pos: f64, along_axis_size: f64, cross_axis_pos: f64, cross_axis_size: f64) rl.Rectangle { + const pos = self.getPoint(along_axis_pos, cross_axis_pos); + const corner = self.getPoint(along_axis_pos + along_axis_size, cross_axis_pos + cross_axis_size); + var rect = rl.Rectangle{ + .x = pos.x, + .y = pos.y, + .width = corner.x - pos.x, + .height = corner.y - pos.y + }; + + if (rect.width < 0) { + rect.x += rect.width; + rect.width *= -1; + } + + if (rect.height < 0) { + rect.y += rect.height; + rect.height *= -1; + } + + return rect; + } + + fn drawLine(self: *RulerContext, along_axis_pos: f64, cross_axis_size: f64, color: rl.Color) void { + rl.drawLineV( + self.getPoint(along_axis_pos, 0), + self.getPoint(along_axis_pos, cross_axis_size), + color + ); + } + + fn drawTicks(self: *RulerContext, from: f64, to: f64, step: f64, line_size: f64, color: rl.Color) void { + var position = from; + while (position < to) : (position += step) { + self.drawLine(position, line_size, color); } } +}; - const step = full_range.size() / subdivisions; +fn drawRulerTicks(_ctx: ?*anyopaque, box: *UI.Box) void { + const ctx: *RulerContext = @ptrCast(@alignCast(_ctx)); + ctx.rect = box.rect(); - { - _ = self.app.ui.createBox(.{ - .background = srcery.yellow, - .float_rect = self.getLineOnRuler(view_id, ruler, axis, full_range.lower, 0, 0.75), - .float_relative_to = ruler - }); + ctx.drawLine(ctx.available_range.lower, 1, srcery.yellow); + ctx.drawLine(ctx.available_range.upper, 1, srcery.yellow); + + if (ctx.available_range.hasExclusive(0)) { + ctx.drawLine(0, 0.75, srcery.yellow); } - { - _ = self.app.ui.createBox(.{ - .background = srcery.yellow, - .float_rect = self.getLineOnRuler(view_id, ruler, axis, full_range.upper, 0, 0.75), - .float_relative_to = ruler - }); + var one_unit = ctx.one_unit orelse return; + + while (ctx.render_range.size() < 5*one_unit) { + one_unit /= 2; } - if (full_range.hasExclusive(0)) { - _ = ui.createBox(.{ - .background = srcery.yellow, - .float_rect = self.getLineOnRuler(view_id, ruler, axis, 0, 0, 0.75), - .float_relative_to = ruler - }); + while (ctx.render_range.size() > 50*one_unit) { + one_unit *= 2; } - const ticks_range = view_range.grow(step).intersectPositive(full_range); + var ticks_range = ctx.render_range.grow(one_unit).intersectPositive(ctx.available_range); + ticks_range.lower = utils.roundNearestTowardZero(f64, ticks_range.lower, one_unit); - self.showRulerTicksRange( - view_id, - ruler, - axis, - utils.roundNearestTowardZero(f64, ticks_range.lower, step) + step/2, - ticks_range.upper, - step, - 0.5 - ); - - self.showRulerTicksRange( - view_id, - ruler, - axis, - utils.roundNearestTowardZero(f64, ticks_range.lower, step), - ticks_range.upper, - step, - 0.25 - ); + ctx.drawTicks(ticks_range.lower, ticks_range.upper, one_unit, 0.5, srcery.yellow); + ctx.drawTicks(ticks_range.lower + one_unit/2, ticks_range.upper, one_unit, 0.25, srcery.yellow); } fn addRulerPlaceholder(self: *MainScreen, key: UI.Key, axis: UI.Axis) *UI.Box { @@ -428,7 +355,7 @@ fn addRulerPlaceholder(self: *MainScreen, key: UI.Key, axis: UI.Axis) *UI.Box { var ruler = ui.createBox(.{ .key = key, .background = srcery.hard_black, - .flags = &.{ .clip_view, .clickable, .scrollable }, + .flags = &.{ .clickable, .scrollable, .clip_view }, .hot_cursor = .mouse_cursor_pointing_hand }); if (axis == .X) { @@ -442,15 +369,42 @@ fn addRulerPlaceholder(self: *MainScreen, key: UI.Key, axis: UI.Axis) *UI.Box { return ruler; } -fn showRuler(self: *MainScreen, ruler: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: UI.Axis) void { +fn formatDuration(allocator: std.mem.Allocator, total_seconds: f64) ![]u8 { + const seconds = @mod(total_seconds, @as(f64, @floatFromInt(std.time.s_per_min))); + const minutes = total_seconds / std.time.s_per_min; + return try std.fmt.allocPrint(allocator, "{d:.0}m {d:.3}s", .{ minutes, seconds }); +} + +fn showRuler(self: *MainScreen, ruler: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: UI.Axis) !void { var ui = &self.app.ui; const view = self.app.getView(view_id) orelse return; + var ruler_ctx = RulerContext.init(axis, &self.app.project, view_id); + var graph_ctx = ruler_ctx; + ruler_ctx.rect = .{ + .x = 0, + .y = 0, + .width = ruler.persistent.size.x, + .height = ruler.persistent.size.y + }; + graph_ctx.rect = .{ + .x = 0, + .y = 0, + .width = graph_box.persistent.size.x, + .height = graph_box.persistent.size.y + }; + ruler.beginChildren(); defer ruler.endChildren(); - self.showRulerTicks(view_id, axis); + const ctx = try ui.frameAllocator().create(RulerContext); + ctx.* = RulerContext.init(axis, &self.app.project, view_id); + + ruler.draw = .{ + .ctx = ctx, + .do = drawRulerTicks + }; const signal = ui.signal(ruler); const mouse_position = switch (axis) { @@ -480,12 +434,16 @@ fn showRuler(self: *MainScreen, ruler: *UI.Box, graph_box: *UI.Box, view_id: Id, const project = &self.app.project; if (view.getAvailableView(axis).hasInclusive(mouse_position_on_graph)) { + const sample_rate = project.getSampleRate(); + if (axis == .Y and view.unit != null) { const unit_name = view.unit.?.name() orelse "Unknown"; _ = ui.label("{s}: {d:.3}", .{unit_name, mouse_position_on_graph}); - } else if (axis == .X and project.sample_rate != null) { - const sample_rate = project.sample_rate.?; - _ = ui.label("{d:.3}s", .{mouse_position_on_graph / sample_rate}); + } else if (axis == .X and sample_rate != null) { + const seconds = mouse_position_on_graph / sample_rate.?; + const frame_allocator = ui.frameAllocator(); + _ = ui.label("{s}", .{ formatDuration(frame_allocator, seconds) catch "-" }); + } else { _ = ui.label("{d:.3}", .{mouse_position_on_graph}); } @@ -510,13 +468,13 @@ fn showRuler(self: *MainScreen, ruler: *UI.Box, graph_box: *UI.Box, view_id: Id, if (zoom_start != null) { _ = ui.createBox(.{ .background = srcery.green, - .float_rect = self.getLineOnRuler(view_id, ruler, axis, zoom_start.?, 0, 1), + .float_rect = ruler_ctx.getLine(zoom_start.?, 1), .float_relative_to = ruler, }); _ = ui.createBox(.{ .background = srcery.green, - .float_rect = self.getLineOnRuler(view_id, graph_box, axis, zoom_start.?, 0, 1), + .float_rect = graph_ctx.getLine(zoom_start.?, 1), .float_relative_to = graph_box, .parent = graph_box }); @@ -525,13 +483,13 @@ fn showRuler(self: *MainScreen, ruler: *UI.Box, graph_box: *UI.Box, view_id: Id, if (zoom_end != null) { _ = ui.createBox(.{ .background = srcery.green, - .float_rect = self.getLineOnRuler(view_id, ruler, axis, zoom_end.?, 0, 1), + .float_rect = ruler_ctx.getLine(zoom_end.?, 1), .float_relative_to = ruler, }); _ = ui.createBox(.{ .background = srcery.green, - .float_rect = self.getLineOnRuler(view_id, graph_box, axis, zoom_end.?, 0, 1), + .float_rect = graph_ctx.getLine(zoom_end.?, 1), .float_relative_to = graph_box, .parent = graph_box }); @@ -541,15 +499,7 @@ fn showRuler(self: *MainScreen, ruler: *UI.Box, graph_box: *UI.Box, view_id: Id, _ = ui.createBox(.{ .background = srcery.green.alpha(0.5), .float_relative_to = ruler, - .float_rect = self.getRectOnRuler( - view_id, - ruler, - axis, - zoom_start.?, - zoom_end.? - zoom_start.?, - 0, - 1 - ) + .float_rect = ruler_ctx.getRect(zoom_start.?, zoom_end.? - zoom_start.?, 0, 1), }); } @@ -595,14 +545,13 @@ fn showView(self: *MainScreen, view_id: Id, height: UI.Sizing) !void { .key = UI.Key.initUsize(view_id.asInt()), .layout_direction = .top_to_bottom, .size_x = UI.Sizing.initGrowFull(), - .size_y = height + .size_y = height, }); view_box.beginChildren(); defer view_box.endChildren(); const toolbar = ui.createBox(.{ .layout_direction = .left_to_right, - .layout_gap = 16, .background = srcery.hard_black, .size_x = UI.Sizing.initGrowFull(), .size_y = UI.Sizing.initFixed(.{ .pixels = ui.rem(2) }) @@ -611,9 +560,36 @@ fn showView(self: *MainScreen, view_id: Id, height: UI.Sizing) !void { toolbar.beginChildren(); defer toolbar.endChildren(); + const view = self.app.getView(view_id).?; var view_name: ?[]const u8 = null; - const view = self.app.getView(view_id).?; + { + const btn = ui.textButton("Settings"); + btn.background = srcery.hard_black; + if (self.view_settings != null and self.view_settings.?.eql(view_id)) { + btn.borders.bottom = .{ + .color = srcery.green, + .size = 4 + }; + } + + if (ui.signal(btn).clicked()) { + if (self.view_settings != null and self.view_settings.?.eql(view_id)) { + self.view_settings = null; + } else { + self.view_settings = view_id; + } + } + } + + { + const btn = ui.textButton("Reset view"); + btn.background = srcery.hard_black; + if (ui.signal(btn).clicked()) { + self.pushViewMoveCommand(view_id, view.available_x_range, view.available_y_range); + } + } + if (view.reference == .channel) { const channel_id = view.reference.channel; const channel = self.app.getChannel(channel_id).?; @@ -737,8 +713,8 @@ fn showView(self: *MainScreen, view_id: Id, height: UI.Sizing) !void { x_ruler = self.addRulerPlaceholder(ui.keyFromString("X ruler"), .X); } - self.showRuler(x_ruler, graph_box, view_id, .X); - self.showRuler(y_ruler, graph_box, view_id, .Y); + try self.showRuler(x_ruler, graph_box, view_id, .X); + try self.showRuler(y_ruler, graph_box, view_id, .Y); } } @@ -936,22 +912,89 @@ pub fn showSidePanel(self: *MainScreen) !void { .right = .{ .color = srcery.hard_black, .size = 4 } }, .layout_direction = .top_to_bottom, - .padding = UI.Padding.all(ui.rem(1)) + .padding = UI.Padding.all(ui.rem(1)), + .layout_gap = ui.rem(0.2) }); container.beginChildren(); defer container.endChildren(); const project = &self.app.project; + const sample_rate = project.getSampleRate(); + + if (self.view_settings) |view_id| { + const view = project.views.get(view_id) orelse return; + + { + const label = ui.label("Settings", .{}); + label.borders.bottom = .{ + .color = srcery.bright_white, + .size = 1 + }; + } + + _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) }); + + var sample_count: ?usize = null; + switch (view.reference) { + .channel => |channel_id| { + const channel = project.channels.get(channel_id).?; + const channel_name = utils.getBoundedStringZ(&channel.name); + const channel_type = NIDaq.getChannelType(channel_name); + const samples = channel.collected_samples.items; + + _ = ui.label("Channel: {s}", .{ channel_name }); + + if (channel_type != null) { + _ = ui.label("Type: {s}", .{ channel_type.?.name() }); + } else { + _ = ui.label("Type: unknown", .{ }); + } + + sample_count = samples.len; + }, + .file => |file_id| { + const file = project.files.get(file_id).?; + + if (file.samples) |samples| { + sample_count = samples.len; + } + } + + } + + if (sample_count != null) { + _ = ui.label("Samples: {d}", .{ sample_count.? }); + + var duration_str: []const u8 = "-"; + if (sample_rate != null) { + const duration = @as(f64, @floatFromInt(sample_count.?)) / sample_rate.?; + if (formatDuration(ui.frameAllocator(), duration)) |str| { + duration_str = str; + } else |_| {} + } + _ = ui.label("Duration: {s}", .{ duration_str }); + } + + + } else { + { + const label = ui.label("Project", .{}); + label.borders.bottom = .{ + .color = srcery.bright_white, + .size = 1 + }; + } + + _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) }); - { var placeholder: ?[]const u8 = null; - if (project.getDefaultSampleRate()) |sample_rate| { - placeholder = try std.fmt.allocPrint(frame_allocator, "{d}", .{ sample_rate }); + if (project.getDefaultSampleRate()) |default_sample_rate| { + placeholder = try std.fmt.allocPrint(frame_allocator, "{d}", .{ default_sample_rate }); } var initial: ?[]const u8 = null; - if (project.sample_rate) |sample_rate| { - initial = try std.fmt.allocPrint(frame_allocator, "{d}", .{ sample_rate }); + if (project.sample_rate) |selected_sample_rate| { + initial = try std.fmt.allocPrint(frame_allocator, "{d}", .{ selected_sample_rate }); } _ = ui.label("Sample rate", .{}); @@ -960,13 +1003,14 @@ pub fn showSidePanel(self: *MainScreen) !void { .storage = &self.sample_rate_input, .placeholder = placeholder, .initial = initial, - .invalid = self.parsed_sample_rate != project.sample_rate + .invalid = self.parsed_sample_rate != project.sample_rate, + .editable = !self.app.isCollectionInProgress() }); project.sample_rate = self.parsed_sample_rate; if (project.getAllowedSampleRates()) |allowed_sample_rates| { - if (project.sample_rate) |sample_rate| { - if (!allowed_sample_rates.hasInclusive(sample_rate)) { + if (project.sample_rate) |selected_sample_rate| { + if (!allowed_sample_rates.hasInclusive(selected_sample_rate)) { project.sample_rate = null; } } @@ -1117,6 +1161,8 @@ pub fn tick(self: *MainScreen) !void { self.closeModal(); } else if (self.fullscreen_view != null) { self.fullscreen_view = null; + } else if (self.view_settings != null) { + self.view_settings = null; } else { self.app.should_close = true; } diff --git a/src/ui.zig b/src/ui.zig index 07ca0a5..0f9935a 100644 --- a/src/ui.zig +++ b/src/ui.zig @@ -476,6 +476,11 @@ pub const Box = struct { pub const Flags = std.EnumSet(Flag); + pub const Draw = struct { + ctx: ?*anyopaque = null, + do: *const fn(ctx: ?*anyopaque, box: *Box) void + }; + const max_wrapped_lines = 64; ui: *UI, @@ -507,6 +512,7 @@ pub const Box = struct { scientific_number: ?f64 = null, scientific_precision: u32 = 1, float_relative_to: ?*Box = null, + draw: ?Draw = null, // Variables that you probably shouldn't be touching last_used_frame: u64 = 0, @@ -725,6 +731,7 @@ pub const BoxOptions = struct { float_relative_to: ?*Box = null, parent: ?*UI.Box = null, texture_color: ?rl.Color = null, + draw: ?Box.Draw = null }; pub const root_box_key = Key.initString(0, "$root$"); @@ -1531,6 +1538,7 @@ pub fn createBox(self: *UI, opts: BoxOptions) *Box { .scientific_precision = opts.scientific_precision orelse 1, .float_relative_to = opts.float_relative_to, .texture_color = opts.texture_color, + .draw = opts.draw, .last_used_frame = self.frame_index, .key = key, @@ -1683,6 +1691,10 @@ fn drawBox(self: *UI, box: *Box) void { ); } + if (box.draw) |box_draw| { + box_draw.do(box_draw.ctx, box); + } + const alignment_x_coeff = box.alignment.x.getCoefficient(); const alignment_y_coeff = box.alignment.y.getCoefficient(); @@ -2206,6 +2218,7 @@ pub const TextInputStorage = struct { pub const TextInputOptions = struct { key: Key, storage: *TextInputStorage, + editable: bool = true, initial: ?[]const u8 = null, placeholder: ?[]const u8 = null, @@ -2216,6 +2229,7 @@ pub const NumberInputOptions = struct { key: Key, storage: *TextInputStorage, invalid: bool = false, + editable: bool = true, initial: ?[]const u8 = null, placeholder: ?[]const u8 = null, @@ -2423,7 +2437,7 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void { } const container_signal = self.signal(container); - if (container_signal.hot) { + if (opts.editable and container_signal.hot) { container.borders = UI.Borders.all(.{ .color = srcery.red, .size = 2 @@ -2467,7 +2481,7 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void { } } - if (container_signal.active) { + if (opts.editable and container_signal.active) { storage.editing = true; } @@ -2623,7 +2637,6 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void { } try storage.insertSingle(storage.cursor_start, @intCast(char)); - no_blinking = true; } } @@ -2640,6 +2653,10 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void { if (container_signal.clicked_outside and !container_signal.is_mouse_inside) { storage.editing = false; } + + if (!opts.editable) { + storage.editing = false; + } } } @@ -2651,7 +2668,8 @@ pub fn numberInput(self: *UI, T: type, opts: NumberInputOptions) !?T { .storage = opts.storage, .initial = opts.initial, .text_color = opts.text_color, - .placeholder = opts.placeholder + .placeholder = opts.placeholder, + .editable = opts.editable }; var is_invalid = opts.invalid; diff --git a/tools/generate.zig b/tools/generate.zig new file mode 100644 index 0000000..fb60792 --- /dev/null +++ b/tools/generate.zig @@ -0,0 +1,44 @@ +const std = @import("std"); + +pub fn show_usage() void { + std.debug.print("Usage: zig build generate \n", .{}); +} + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + defer _ = gpa.deinit(); + + var args = try std.process.argsWithAllocator(allocator); + defer args.deinit(); + + _ = args.next(); + + const sample_rate_str = args.next() orelse { + show_usage(); + std.process.exit(1); + }; + + const sample_count_str = args.next() orelse { + show_usage(); + std.process.exit(1); + }; + + const filename = args.next() orelse { + show_usage(); + std.process.exit(1); + }; + + const sample_rate = try std.fmt.parseFloat(f64, sample_rate_str); + const sample_count = try std.fmt.parseInt(u32, sample_count_str, 10); + + const f = try std.fs.cwd().createFile(filename, .{ .exclusive = true }); + defer f.close(); + + for (0..sample_count) |i| { + const i_f64: f64 = @floatFromInt(i); + const sample: f64 = std.math.sin( i_f64 / sample_rate * std.math.pi * 2 ) * 10; + const sample_bytes = std.mem.toBytes(sample); + try f.writeAll(&sample_bytes); + } +} \ No newline at end of file