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