mcap/src/ui/imgui.jai
Musa Mahmood be9ad707c4 [0.1.17] mcap.exe FILE opens a file, beside a running mcap in view mode; a Command Line window; thumbnails no longer stuck on "Cannot read"
mcap.exe FILE, with FILE first (as Explorer's Open with passes it), opens an .mcap, image, video or
.snagx (app/open_file.jai). With no mcap running, the app starts as usual: an .mcap in the library
opens in the Capture window, anything else in the Opened File window, which says where the library
has it already (by its hash in .import-index) or imports it into the library folder typed or chosen.
With mcap running, a second window opens in view mode: the file alone, with Import, and Open in mcap
/ Show in mcap, which hand it to the running copy (a new Open_File message) and close the view. The
library is not loaded or watched, no model loads, and it has no tray icon, no global hotkeys, and
writes no settings or layout.

Import takes a single file as its source too (and -import FILE DEST); an .mcap is copied in as it is,
while a folder's import still skips them. -open-import DEST imports the opened file as its Import
button does, then exits.

-h and --help open a Command Line window listing the options, also toggled from the Debug Panel; with
mcap running, that one comes forward with it (a Show_Help message).

Thumbnails: the "First preview shown" log line, put between creating a preview's texture and its
else, had every preview after the first marked failed. Once its texture was released (scrolled far
away, or the switch from WARP to the GPU at startup) it was never decoded again and showed "Cannot
read".
2026-10-14 08:00:02 +01:00

315 lines
11 KiB
Plaintext

app_imgui_init :: () { using ImGui;
log("ImGui Version %s (docking branch) \n", to_string(ImGui.GetVersion()));
app.imgui_context = ImGui.CreateContext();
io := ImGui.GetIO();
io.ConfigFlags_ |= .DockingEnable | .ViewportsEnable | .DpiEnableScaleViewports | .DpiEnableScaleFonts;
assert(app.win_scale != 0, "Window scaling must be set on startup!");
// The theme lives in app_theme.master unscaled; app_theme_bake() is what scales it onto the
// live style, and it can be called again whenever win_scale changes.
app_theme_build_builtin(.Default, *app_theme.master);
app_theme.ref = app_theme.master;
app_theme.palette = app_theme_default_palette();
app_theme_bake(app.win_scale);
style := ImGui.GetStyle();
style.FontScaleMain = 1.0; // the user's scale; the DPI comes in through FontScaleDpi alone
style.FontScaleDpi = app.win_scale; // Set initial font scale. (in docking branch: using io.ConfigDpiScaleFonts=true automatically overrides this for every window depending on the current monitor)
io.ConfigDpiScaleFonts = true; // [Experimental] Automatically overwrite style.FontScaleDpi in Begin() when Monitor DPI changes. This will scale fonts but _NOT_ scale sizes/padding for now.
io.ConfigDpiScaleViewports = true; // [Experimental] Scale Dear ImGui and Platform Windows when Monitor DPI changes.
io.ConfigNavEscapeClearFocusItem = false; // We don't want imgui capturing our Escape commands!
io.ConfigNavEscapeClearFocusWindow = false; // We don't want imgui capturing our Escape commands!
io.ConfigInputTextEnterKeepActive = true; // Keeps active + I modified source so it doesn't select all because that is retarded.
}
// Floating ImGui windows have OS windows of their own, which the frame loop stops updating while
// the app is in the tray, so they are hidden with the main window and shown again with it.
imgui_floating_windows_hidden: bool;
imgui_show_floating_windows :: (visible: bool) {
if visible == !imgui_floating_windows_hidden return;
imgui_floating_windows_hidden = !visible;
viewports := ImGui.GetPlatformIO().Viewports;
main_viewport := ImGui.GetMainViewport();
for 0..viewports.Size - 1 {
viewport := viewports.Data[it];
if viewport == main_viewport continue;
#if USING_WINDOWS {
if viewport.PlatformHandleRaw ShowWindow(cast(HWND) viewport.PlatformHandleRaw, cast(s32) ifx visible then SW_SHOWNA else SW_HIDE);
} else {
window := SDL_GetWindowFromID(cast(SDL_WindowID) cast(u64) viewport.PlatformHandle);
if !window continue;
if visible SDL_ShowWindow(window);
else SDL_HideWindow(window);
}
}
}
app_imgui_finish :: () {
ImGui.DestroyContext(app.imgui_context);
}
app_imgui_config_setup :: () {
push_allocator(context.default_allocator);
app_program_data_path := app_program_data_dir();
log("program data path: %", app_program_data_path);
#if DEBUG {
app_imgui_ini_path := sprint("%/imgui-debug.ini\0", app_program_data_path);
} else {
app_imgui_ini_path := sprint("%/imgui.ini\0", app_program_data_path);
}
this_allocation_is_not_a_leak(app_imgui_ini_path.data);
io := ImGui.GetIO();
// View mode (app/open_file.jai) is one window filling the main one: no layout to load, and the
// running instance's is left alone.
if app.view_mode { io.IniFilename = null; return; }
io.IniFilename = app_imgui_ini_path.data;
// Loaded before the first frame, so ImGui does not go looking for the file itself.
if !file_exists(to_string(io.IniFilename)) imgui_set_default_layout("no layout file yet");
}
// The window layout a first run starts with, and what the debug panel's Reset Layout restores.
DEFAULT_IMGUI_INI_NAME :: "default_imgui.ini";
DEFAULT_IMGUI_INI_DATA :: #run,host -> string { #import "File"; return read_entire_file("assets/data/default_imgui.ini"); };
// Every layout the app itself sets comes through here and is logged; ImGui's own autosaves are not.
// The file gets the default verbatim, rather than waiting for ImGui's next autosave to write it.
imgui_set_default_layout :: (reason: string) -> written: bool {
ImGui.LoadIniSettingsFromMemory(DEFAULT_IMGUI_INI_DATA.data, xx DEFAULT_IMGUI_INI_DATA.count);
ini_path := to_string(ImGui.GetIO().IniFilename);
if !ini_path {
log("[layout] Set the default layout (%); no layout file to write.", reason);
return false;
}
if !write_entire_file(ini_path, DEFAULT_IMGUI_INI_DATA) {
log_error("[layout] Set the default layout (%), but could not write it to %.", reason, ini_path);
return false;
}
log("[layout] Set the default layout (%) and wrote it to %.", reason, ini_path);
return true;
}
imgui_reset_layout_requested: bool;
// Run between frames: loading settings tears down and rebuilds the dock nodes the windows sit in.
imgui_reset_layout_if_requested :: () {
if !imgui_reset_layout_requested return;
imgui_reset_layout_requested = false;
if imgui_set_default_layout("Reset Layout") toast_show(.Success, "Layout reset to the default");
else toast_show(.Error, "Layout reset, but it could not be saved. See the Event Log (%).", to_string(toggle_logger_panel_hotkey));
}
imgui_scale :: imgui_scale_with_font_and_dpi;
imgui_scale_with_font_and_dpi :: inline (input: $T) -> T {
style := ImGui.GetStyle();
return input * style.FontScaleDpi * style.FontScaleMain;
}
push_imgui_window :: (label: string, flags: ImGui.WindowFlags=.None, p_open: *bool = null) #expand {
ImGui.Begin(to_c_string(label,, temp), p_open, flags);
`defer ImGui.End();
`defer imgui_draw_active_window_border(label);
}
imgui_draw_active_window_border :: (label: string) {
using ImGui;
focused := IsWindowFocused(.RootAndChildWindows);
keyboard_owner_note(label, focused);
if !focused return;
thickness := imgui_scale(app_theme.palette.active_window_border_size);
if thickness <= 0 return;
rounding := ifx IsWindowDocked() then 0.0 else GetStyle().WindowRounding;
pos := GetWindowPos();
size := GetWindowSize();
inset := thickness * 0.5;
draw_list := GetWindowDrawList();
// By End() the clip rect is the content area, which would cut off the window's edge.
PushClipRect(pos, .{pos.x + size.x, pos.y + size.y}, false);
draw_list.AddRect(draw_list, .{pos.x + inset, pos.y + inset}, .{pos.x + size.x - inset, pos.y + size.y - inset},
GetColorU32(app_theme.palette.active_window_border), rounding=rounding, thickness=thickness);
PopClipRect();
}
// Returns row count to be scrolled by the mouse wheel, for tables that scroll by row
// Call inside the table's window: the wheel only counts while that window is hovered.
// Touchpads send fractions of a notch, which add up in remainder instead of truncating to zero.
imgui_wheel_rows :: (remainder: *float) -> s32 {
if !ImGui.IsWindowHovered(.RootAndChildWindows) {
remainder.* = 0;
return 0;
}
remainder.* += ImGui.GetIO().MouseWheel;
rows := cast(s32) remainder.*; // truncates toward zero, so it works both ways
remainder.* -= cast(float) rows;
return rows;
}
imgui_viewport_center :: inline () -> Vector2 {
using viewport := ImGui.GetMainViewport();
return Vector2.{ Pos.x + Size.x * 0.5, Pos.y + Size.y * 0.5 };
}
imgui_show_modal :: (label: string) #expand { using ImGui;
OpenPopup(to_c_string(label,, temp));
center := imgui_viewport_center();
pivot := Vector2.{0.5, 0.5};
SetNextWindowPos(center, .Always, pivot);
}
imgui_text_size :: (s: string) -> Vector2 {
return ImGui.CalcTextSize(s.data, s.data + s.count);
}
imgui_text_top_right :: (text: string, text_color: Vector4) {
using ImGui;
original_cursor_pos := GetCursorPos();
windowRegionMax := GetWindowContentRegionMax();
text_size := CalcTextSize(text);
TOP_RIGHT_PADDING : float32 : 10;
cursorX: float32 = windowRegionMax.x - text_size.x - TOP_RIGHT_PADDING;
cursorY: float32 = 0 + TOP_RIGHT_PADDING;
cursor: Vector2 = {cursorX, cursorY};
SetCursorPos(cursor);
TextColored(text_color, "%", text);
SetCursorPos(original_cursor_pos); // restore cursor position.
}
imgui_show_text_with_highlights :: (text: string, highlights: []bool, default_color: Vector4, highlight_color: Vector4) {
using ImGui;
draw_list := GetWindowDrawList();
pos := GetCursorScreenPos();
font := GetFont();
font_size := GetFontSize();
text_size := CalcTextSize(text);
Dummy(text_size);
default_u32 := GetColorU32(default_color);
highlight_u32 := GetColorU32(highlight_color);
// Highlights are per byte; a codepoint takes the state of its lead byte, so a run
// only ends at a byte that is not a UTF-8 continuation byte.
run_start := 0;
while run_start < text.count {
highlighted := run_start < highlights.count && highlights[run_start];
run_end := run_start + 1;
while run_end < text.count {
is_lead_byte := (text[run_end] & 0xC0) != 0x80;
if is_lead_byte && (run_end < highlights.count && highlights[run_end]) != highlighted break;
run_end += 1;
}
run := slice(text, run_start, run_end - run_start);
color := ifx highlighted then highlight_u32 else default_u32;
draw_list.AddText(draw_list, font, font_size, pos, color, run);
pos.x += CalcTextSize(run).x;
run_start = run_end;
}
}
CenteredButton :: (label: string) -> bool { using ImGui;
ALIGNMENT :: 0.5;
label_c := to_c_string(label,, temp);
style := GetStyle();
size := CalcTextSize(label_c).x + style.FramePadding.x * 2.0;
available := GetContentRegionAvail().x;
off := (available - size) * ALIGNMENT;
if (off > 0.0) {
SetCursorPosX(GetCursorPosX() + off);
}
return Button(label_c);
}
RightAlignedText :: (text: string, text_color: Vector4) {
using ImGui;
text_size := CalcTextSize(text);
available := GetWindowContentRegionMax().x;
TOP_RIGHT_PADDING : float32 : 10;
// cursorX: float32 = available.x - text_size.x - TOP_RIGHT_PADDING;
// cursorY: float32 = 0 + TOP_RIGHT_PADDING;
// cursor: Vector2 = {cursorX, cursorY};
off := available - text_size.x - TOP_RIGHT_PADDING;
if (off > 0.0) {
SetCursorPosX(GetCursorPosX() + off);
}
TextColored(text_color, "%", text);
}
RightAlignedButton :: (label: string) -> bool #expand { using ImGui;
// start := GetCursorPos();
label_c := to_c_string(label,, temp);
style := GetStyle();
size := CalcTextSize(label_c).x + style.FramePadding.x * 2.0;
available := GetContentRegionAvail().x;
off := (available - size);
if (off > 0.0) {
SetCursorPosX(GetCursorPosX() + off);
}
result := Button(label_c);
// SetCursorPos(start);
return result;
}
ButtonTooltip :: (desc: string) {
if ImGui.IsItemHovered(.DelayShort) {
ImGui.SetTooltip("%", desc);
}
}
// We have this because we cannot print `%` symbols using print_to_builder, so we have to use TextUnformatted
TextColoredAlt :: inline (col: Vector4, s: string) { using ImGui;
PushStyleColor(.Text, col); defer PopStyleColor();
TextUnformatted(s);
}
enum_c_string :: ($T: Type, replace_underscores:=true, replace_with:=#char " ") -> (c_string: *u8) #expand {
push_allocator(temp);
// e.g. turns enum { APPLE; BANANA; } into "APPLE\0BANANA\0"
builder: String_Builder;
for enum_values_as_s64(T) {
item_str := tprint("%\0", cast(T) it);
append(*builder, item_str);
}
append(*builder, "\0");
result := builder_to_string(*builder);
for 0..result.count-1 {
if result[it] == #char "_" {
result[it] = replace_with;
}
}
return result.data;
} @Utility