Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,19 @@ curl -LsSf https://spacecap.org/install | sh -s -- --uninstall

- Desktop/window capture.
- Screen recording.
- Screenshots.
- Replay buffer - save last n seconds of video (buffered in memory).
- Capture preview.
- Global keybinds.

## Roadmap

- ~~Screenshots~~
- File browser
- Video editor
- Trim clips
- Export in a variety of formats
- Adjust audio levels
- Windows support
- File browser.
- Video editor.
- Trim clips.
- Export in a variety of formats.
- Adjust audio levels.
- Windows support.

## Requirements

Expand Down
61 changes: 36 additions & 25 deletions src/ui/draw_bottom_panel.zig
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const AudioDevice = @import("../store/audio_session.zig").AudioDevice;
const UIStorage = @import("./ui_storage.zig").UIStorage;
const imgui_util = @import("./imgui_util.zig");
const util = @import("../util.zig");
const theme = @import("./theme.zig");
const Colors = @import("./theme.zig").Colors;

const AUDIO_GAIN_DB_MIN: f32 = -60.0;
const AUDIO_GAIN_DB_MAX: f32 = 12.0;
Expand All @@ -16,6 +16,19 @@ pub fn draw_bottom_panel(allocator: Allocator, ui_storage: *UIStorage, store: *S
_ = c.ImGui_Begin(dockspace.BOTTOM_WINDOW_NAME, null, c.ImGuiWindowFlags_None);
defer c.ImGui_End();

{
_ = c.ImGui_BeginChild(
"##bottom_panel_content",
.{ .x = 0, .y = 0 },
c.ImGuiChildFlags_None,
c.ImGuiWindowFlags_None,
);
defer c.ImGui_EndChild();
try draw_bottom_panel_content(allocator, ui_storage, store, state);
}
}

fn draw_bottom_panel_content(allocator: Allocator, ui_storage: *UIStorage, store: *Store, state: *Store.State) !void {
// ----------------------------------------------------------------------------
// Video collapsing header.
// ----------------------------------------------------------------------------
Expand Down Expand Up @@ -79,18 +92,18 @@ pub fn draw_bottom_panel(allocator: Allocator, ui_storage: *UIStorage, store: *S
c.ImGui_TableNextRow();
_ = c.ImGui_TableNextColumn();
const replay_buffer_button_label = if (state.capture.replay_buffer_active) " Replay Buffer" else " Replay Buffer";
const replay_buffer_button_color = if (state.capture.replay_buffer_active) theme.red.as_vec4() else theme.green.as_vec4();
const replay_buffer_button_hover_color = if (state.capture.replay_buffer_active) theme.light_red.as_vec4() else theme.light_green.as_vec4();
const replay_buffer_button_disabled = !state.capture.replay_buffer_active and !video_capture_ready;
c.ImGui_PushStyleColorImVec4(c.ImGuiCol_Button, replay_buffer_button_color);
c.ImGui_PushStyleColorImVec4(c.ImGuiCol_ButtonHovered, replay_buffer_button_hover_color);
c.ImGui_PushStyleColorImVec4(c.ImGuiCol_ButtonActive, replay_buffer_button_color);
c.ImGui_BeginDisabled(replay_buffer_button_disabled);
if (c.ImGui_ButtonEx(replay_buffer_button_label, .{ .x = c.ImGui_GetContentRegionAvail().x, .y = 0 })) {
store.dispatch(.{ .capture = if (state.capture.replay_buffer_active) .stop_replay_buffer else .start_replay_buffer });

{
imgui_util.push_button_color(if (state.capture.replay_buffer_active) .red else .green);
defer imgui_util.pop_button_color();
c.ImGui_BeginDisabled(replay_buffer_button_disabled);
if (c.ImGui_ButtonEx(replay_buffer_button_label, .{ .x = c.ImGui_GetContentRegionAvail().x, .y = 0 })) {
store.dispatch(.{ .capture = if (state.capture.replay_buffer_active) .stop_replay_buffer else .start_replay_buffer });
}
c.ImGui_EndDisabled();
}
c.ImGui_EndDisabled();
c.ImGui_PopStyleColorEx(3);

_ = c.ImGui_TableNextColumn();
c.ImGui_Text("%.2fMB", state.capture.replay_buffer_metrics.size_in_mb(.total));
if (c.ImGui_BeginItemTooltip()) {
Expand All @@ -110,18 +123,16 @@ pub fn draw_bottom_panel(allocator: Allocator, ui_storage: *UIStorage, store: *S
c.ImGui_TableNextRow();
_ = c.ImGui_TableNextColumn();
const recording_button_label = if (state.capture.recording_to_disk) " Record" else " Record";
const recording_button_color = if (state.capture.recording_to_disk) theme.red.as_vec4() else theme.green.as_vec4();
const recording_button_hover_color = if (state.capture.recording_to_disk) theme.light_red.as_vec4() else theme.light_green.as_vec4();
const recording_button_disabled = !state.capture.recording_to_disk and !video_capture_ready;
c.ImGui_PushStyleColorImVec4(c.ImGuiCol_Button, recording_button_color);
c.ImGui_PushStyleColorImVec4(c.ImGuiCol_ButtonHovered, recording_button_hover_color);
c.ImGui_PushStyleColorImVec4(c.ImGuiCol_ButtonActive, recording_button_color);
c.ImGui_BeginDisabled(recording_button_disabled);
if (c.ImGui_ButtonEx(recording_button_label, .{ .x = c.ImGui_GetContentRegionAvail().x, .y = 0 })) {
store.dispatch(.{ .capture = if (state.capture.recording_to_disk) .stop_recording_to_disk else .start_recording_to_disk });
{
imgui_util.push_button_color(if (state.capture.recording_to_disk) .red else .green);
defer imgui_util.pop_button_color();
const recording_button_disabled = !state.capture.recording_to_disk and !video_capture_ready;
c.ImGui_BeginDisabled(recording_button_disabled);
if (c.ImGui_ButtonEx(recording_button_label, .{ .x = c.ImGui_GetContentRegionAvail().x, .y = 0 })) {
store.dispatch(.{ .capture = if (state.capture.recording_to_disk) .stop_recording_to_disk else .start_recording_to_disk });
}
c.ImGui_EndDisabled();
}
c.ImGui_EndDisabled();
c.ImGui_PopStyleColorEx(3);
_ = c.ImGui_TableNextColumn();
c.ImGui_Text("%.2fMB", state.capture.recording_metrics.size_in_mb(.total));
if (c.ImGui_BeginItemTooltip()) {
Expand Down Expand Up @@ -338,11 +349,11 @@ fn draw_audio_device_audio_level(ui_storage: *UIStorage, audio_device: *AudioDev
// Pick the meter color and draw the progress bar.
// ----------------------------------------------------------------------------
const color = if (audio_level_linear >= 0.9)
theme.red.as_vec4()
Colors.red.as_vec4()
else if (audio_level_linear >= 0.75)
theme.accent.as_vec4()
Colors.accent.as_vec4()
else
theme.green.as_vec4();
Colors.green.as_vec4();
const size = c.ImVec2{
.x = c.ImGui_GetContentRegionAvail().x,
.y = c.ImGui_GetFrameHeight(),
Expand Down
80 changes: 61 additions & 19 deletions src/ui/draw_left_column.zig
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const std = @import("std");
const build_options = @import("build_options");
const c = @import("imguiz").imguiz;
const imgui_util = @import("./imgui_util.zig");
const util = @import("../util.zig");
Expand Down Expand Up @@ -35,36 +36,65 @@ pub fn draw_left_column(allocator: std.mem.Allocator, store: *Store, state: *Sto
_ = c.ImGui_Begin(dockspace.LEFT_WINDOW_NAME, null, c.ImGuiWindowFlags_None);
defer c.ImGui_End();

if (c.ImGui_BeginTabBar("MainTabBar", 0)) {
defer c.ImGui_EndTabBar();

if (c.ImGui_BeginTabItem(" Settings", null, 0)) {
defer c.ImGui_EndTabItem();
const footer_height = c.ImGui_GetFrameHeight();
// ----------------------------------------------------------------------------
// Content
// ----------------------------------------------------------------------------
{
_ = c.ImGui_BeginChild(
"##left_column_content",
.{ .x = 0, .y = -footer_height },
c.ImGuiChildFlags_None,
c.ImGuiWindowFlags_None,
);
defer c.ImGui_EndChild();
if (c.ImGui_BeginTabBar("main_tab_bar", 0)) {
defer c.ImGui_EndTabBar();

try draw_capture_settings(allocator, store, state);
if (c.ImGui_BeginTabItem(" Settings", null, 0)) {
defer c.ImGui_EndTabItem();

c.ImGui_Dummy(.{ .x = 0, .y = GROUP_SPACING });
try draw_capture_settings(allocator, store, state);

try draw_output_settings(allocator, store);
c.ImGui_Dummy(.{ .x = 0, .y = GROUP_SPACING });

c.ImGui_Dummy(.{ .x = 0, .y = GROUP_SPACING });
draw_misc_settings();
try draw_output_settings(allocator, store);

if (util.DEBUG) {
c.ImGui_Dummy(.{ .x = 0, .y = GROUP_SPACING });
c.ImGui_SeparatorText("IMGUI Debug");
draw_misc_settings();

if (c.ImGui_ButtonEx("Show Demo", .{ .x = c.ImGui_GetContentRegionAvail().x, .y = 0 })) {
store.dispatch(.show_demo);
}
if (util.DEBUG) {
c.ImGui_Dummy(.{ .x = 0, .y = GROUP_SPACING });
c.ImGui_SeparatorText("IMGUI Debug");

if (c.ImGui_ButtonEx("Show Demo", .{ .x = c.ImGui_GetContentRegionAvail().x, .y = 0 })) {
store.dispatch(.show_demo);
}

c.ImGui_Spacing();
const io = c.ImGui_GetIO();
c.ImGui_TextDisabled("%.3f ms/frame", 1000.0 / io.*.Framerate);
c.ImGui_TextDisabled("%.1f fps", io.*.Framerate);
c.ImGui_Spacing();
const io = c.ImGui_GetIO();
c.ImGui_TextDisabled("%.3f ms/frame", 1000.0 / io.*.Framerate);
c.ImGui_TextDisabled("%.1f fps", io.*.Framerate);
}
}
}
}

// ----------------------------------------------------------------------------
// Footer
// ----------------------------------------------------------------------------
{
_ = c.ImGui_BeginChild(
"##left_column_footer",
.{ .x = 0, .y = 0 },
c.ImGuiChildFlags_None,
c.ImGuiWindowFlags_None,
);
defer c.ImGui_EndChild();
const version_label = std.fmt.comptimePrint("{s}", .{build_options.version});
imgui_util.center_next_text(version_label);
c.ImGui_TextDisabled(version_label);
}
}

fn draw_misc_settings() void {
Expand All @@ -75,6 +105,18 @@ fn draw_misc_settings() void {
c.ImGui_OpenPopup(popup_title, c.ImGuiPopupFlags_None);
}

c.ImGui_Dummy(.{ .x = 0, .y = GROUP_SPACING });

const report_an_issue_text = "Report Issue";
imgui_util.center_next_text(report_an_issue_text);
_ = c.ImGui_TextLinkOpenURLEx(report_an_issue_text, "https://github.com/mgerb/spacecap/issues/new");

c.ImGui_Dummy(.{ .x = 0, .y = GROUP_SPACING });

const source_text = " Source";
imgui_util.center_next_text(source_text);
_ = c.ImGui_TextLinkOpenURLEx(source_text, "https://github.com/mgerb/spacecap");

const viewport_size = c.ImGui_GetMainViewport().*.Size;
c.ImGui_SetNextWindowSize(.{ .x = @min(800, viewport_size.x), .y = @min(600, viewport_size.y) }, c.ImGuiCond_Appearing);
var popup_open = true; // Enables the close icon in the top right.
Expand Down
36 changes: 36 additions & 0 deletions src/ui/imgui_util.zig
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const std = @import("std");
const imguiz = @import("imguiz").imguiz;
const Colors = @import("./theme.zig").Colors;

pub const WIDTH_FILL = -std.math.floatMin(f32);

Expand Down Expand Up @@ -31,3 +32,38 @@ pub fn item_tooltip(text: [*:0]const u8) void {
pub fn set_next_item_width_fill() void {
imguiz.ImGui_SetNextItemWidth(WIDTH_FILL);
}

pub fn center_next_text(text: [*:0]const u8) void {
const text_width = imguiz.ImGui_CalcTextSizeEx(text, null, true, -1).x;
const available_width = imguiz.ImGui_GetContentRegionAvail().x;
imguiz.ImGui_SetCursorPosX(
imguiz.ImGui_GetCursorPosX() + @max(0, (available_width - text_width) / 2),
);
}

/// NOTE: Must be followed by `pop_button_color`.
pub fn push_button_color(color: Colors.Enum) void {
const colors = blk: {
switch (color) {
.red => {
break :blk .{ Colors.red.as_vec4(), Colors.light_red.as_vec4() };
},
.green => {
break :blk .{ Colors.green.as_vec4(), Colors.light_green.as_vec4() };
},
.transparent => {
break :blk .{ Colors.transparent.as_vec4(), Colors.transparent.as_vec4() };
},
else => {
@panic("Color not implemented.");
},
}
};
imguiz.ImGui_PushStyleColorImVec4(imguiz.ImGuiCol_Button, colors.@"0");
imguiz.ImGui_PushStyleColorImVec4(imguiz.ImGuiCol_ButtonHovered, colors.@"1");
imguiz.ImGui_PushStyleColorImVec4(imguiz.ImGuiCol_ButtonActive, colors.@"0");
}

pub fn pop_button_color() void {
imguiz.ImGui_PopStyleColorEx(3);
}
99 changes: 53 additions & 46 deletions src/ui/theme.zig
Original file line number Diff line number Diff line change
@@ -1,53 +1,27 @@
const std = @import("std");
const c = @import("imguiz").imguiz;

pub const red = Color.init("#c24d3f");
pub const light_red = Color.init("#cf625a");
pub const green = Color.init("#29ae62");
pub const light_green = Color.init("#36c37d");
pub const light_blue = Color.init("#5bbcff");
pub const blue = Color.init("#3791d2");
pub const dark_blue = Color.init("#2a7cb7");
pub const text = Color.init("#faf4eb");
pub const light_accent = Color.init("#f2e6cf");
pub const accent = Color.init("#e1ccad");
pub const dark_1 = Color.init("#0a0a0a");
pub const dark_2 = Color.init("#212121");
pub const dark_3 = Color.init("#363636");
pub const Colors = struct {
pub const red = Color.init("#c24d3f");
pub const light_red = Color.init("#cf625a");
pub const green = Color.init("#29ae62");
pub const light_green = Color.init("#36c37d");
pub const light_blue = Color.init("#5bbcff");
pub const blue = Color.init("#3791d2");
pub const dark_blue = Color.init("#2a7cb7");
pub const text = Color.init("#faf4eb");
pub const light_accent = Color.init("#f2e6cf");
pub const accent = Color.init("#e1ccad");
pub const dark_1 = Color.init("#0a0a0a");
pub const dark_2 = Color.init("#212121");
pub const dark_3 = Color.init("#363636");
// pub const transparent: Color = .{ .r = 0, .g = 0, .b = 0, .a = 0 };
pub const transparent = Color.init("#00000000");

fn hex_to_imvec4(comptime hex: []const u8) c.ImVec4 {
const start = if (hex.len > 0 and hex[0] == '#') 1 else 0;
const digits = hex.len - start;
if (digits != 6 and digits != 8) {
@compileError("hex color must be rrggbb, #rrggbb, rrggbbaa, or #rrggbbaa");
}

const r = hex_byte(hex[start], hex[start + 1]);
const g = hex_byte(hex[start + 2], hex[start + 3]);
const b = hex_byte(hex[start + 4], hex[start + 5]);
const a = if (digits == 8) hex_byte(hex[start + 6], hex[start + 7]) else 255;

return .{
.x = @as(f32, @floatFromInt(r)) / 255.0,
.y = @as(f32, @floatFromInt(g)) / 255.0,
.z = @as(f32, @floatFromInt(b)) / 255.0,
.w = @as(f32, @floatFromInt(a)) / 255.0,
};
}

fn hex_byte(comptime high: u8, comptime low: u8) u8 {
return hex_digit(high) * 16 + hex_digit(low);
}

fn hex_digit(comptime digit: u8) u8 {
return switch (digit) {
'0'...'9' => digit - '0',
'a'...'f' => digit - 'a' + 10,
'A'...'F' => digit - 'A' + 10,
else => @compileError("hex color contains a non-hex digit"),
};
}
pub const Enum = std.meta.DeclEnum(@This());
};

pub const Color = struct {
const Color = struct {
hex: []const u8,
r: u8,
g: u8,
Expand Down Expand Up @@ -83,4 +57,37 @@ pub const Color = struct {
vec.w = alpha;
return vec;
}

fn hex_to_imvec4(comptime hex: []const u8) c.ImVec4 {
const start = if (hex.len > 0 and hex[0] == '#') 1 else 0;
const digits = hex.len - start;
if (digits != 6 and digits != 8) {
@compileError("hex color must be rrggbb, #rrggbb, rrggbbaa, or #rrggbbaa");
}

const r = hex_byte(hex[start], hex[start + 1]);
const g = hex_byte(hex[start + 2], hex[start + 3]);
const b = hex_byte(hex[start + 4], hex[start + 5]);
const a = if (digits == 8) hex_byte(hex[start + 6], hex[start + 7]) else 255;

return .{
.x = @as(f32, @floatFromInt(r)) / 255.0,
.y = @as(f32, @floatFromInt(g)) / 255.0,
.z = @as(f32, @floatFromInt(b)) / 255.0,
.w = @as(f32, @floatFromInt(a)) / 255.0,
};
}

fn hex_byte(comptime high: u8, comptime low: u8) u8 {
return hex_digit(high) * 16 + hex_digit(low);
}

fn hex_digit(comptime digit: u8) u8 {
return switch (digit) {
'0'...'9' => digit - '0',
'a'...'f' => digit - 'a' + 10,
'A'...'F' => digit - 'A' + 10,
else => @compileError("hex color contains a non-hex digit"),
};
}
};
Loading
Loading