// The Theme Editor panel. Modeled on ImGui::ShowStyleEditor (modules/imgui-source/imgui_demo.cpp // line 8558), which is not in the compiled libs -- imgui_demo.cpp is not built -- so it is // reimplemented here against the bindings we do have. // // Everything in the Sizes and Colors tabs edits app_theme.master, which is DPI-unscaled. The // single app_theme_bake() at the bottom is what puts the result on screen. THEME_EDITOR_SAVE_AS_POPUP :: "Save Theme As##theme_editor"; theme_editor_save_as_modal_shown :: () -> bool { return ImGui.IsPopupOpen(to_c_string(THEME_EDITOR_SAVE_AS_POPUP,, temp)); } app_style_editor_ui :: () { if !app.show_style_editor return; using ImGui; SetNextWindowSize(imgui_scale(Vector2.{560, 680}), .FirstUseEver); title := ifx app_theme.dirty then "Theme Editor *###ThemeEditor" else "Theme Editor###ThemeEditor"; push_imgui_window(title, p_open=*app.show_style_editor); changed := false; style := *app_theme.master; // the unscaled master, NOT GetStyle() ref := *app_theme.ref; live := GetStyle(); // only for widget metrics and the font controls // master is unscaled, so a one-pixel border is literally 1.0 here. DEFAULT_BORDER_SIZE :: 1.0; MAX_BORDER_SIZE :: 2.0; PushItemWidth(GetWindowWidth() * 0.5); defer PopItemWidth(); theme_editor_theme_section(); SeparatorText("General"); ShowFontSelector("Fonts##theme"); // The font fields are per-user and per-monitor rather than part of a theme, so they are // edited on the live style directly, the same way the Debug Panel does it. if DragFloat("FontSizeBase", *live.FontSizeBase, 0.2, 8, 64, "%.0f", .AlwaysClamp) { live._NextFrameFontSizeBase = live.FontSizeBase; } if IsItemDeactivatedAfterEdit() app_save_user_settings_file_and_toast("font size"); DragFloat("FontScaleMain", *live.FontScaleMain, DEFAULT_FONT_SCALE_INCREMENT, FONT_SCALE_MIN, FONT_SCALE_MAX, flags = .AlwaysClamp); if IsItemDeactivatedAfterEdit() app_save_user_settings_file_and_toast("font size"); if SliderFloat("FrameRounding", *style.FrameRounding, 0, 12, "%.0f") { style.GrabRounding = style.FrameRounding; // keep grabs matching frames, as the demo does changed = true; } { border := style.WindowBorderSize > 0; if Checkbox("WindowBorder", *border) { style.WindowBorderSize = ifx border then DEFAULT_BORDER_SIZE else 0.0; changed = true; } } SameLine(); { border := style.FrameBorderSize > 0; if Checkbox("FrameBorder", *border) { style.FrameBorderSize = ifx border then DEFAULT_BORDER_SIZE else 0.0; changed = true; } } SameLine(); { border := style.PopupBorderSize > 0; if Checkbox("PopupBorder", *border) { style.PopupBorderSize = ifx border then DEFAULT_BORDER_SIZE else 0.0; changed = true; } } if Button("Save Ref") ref.* = style.*; SameLine(); if Button("Revert Ref") { style.* = ref.*; changed = true; } SameLine(); ButtonTooltip("Save/Revert against a scratch copy held in memory only. Use Save to write the theme to disk."); SeparatorText("Details"); if BeginTabBar("##theme_tabs") { defer EndTabBar(); if BeginTabItem("Sizes") { changed |= theme_editor_sizes_tab(style, MAX_BORDER_SIZE); EndTabItem(); } if BeginTabItem("Colors") { changed |= theme_editor_colors_tab(style, ref, live); EndTabItem(); } if BeginTabItem("Palette") { changed |= theme_editor_palette_tab(); EndTabItem(); } if BeginTabItem("Rendering") { changed |= theme_editor_rendering_tab(style); EndTabItem(); } } if changed { theme_sanitize_style(style); // a Ctrl+click typed value bypasses the sliders' ranges app_theme.dirty = true; app_theme_bake(app.win_scale); // once, at the end, so the frame stays self-consistent } } // ---------------------------------------------------------------- theme picker theme_editor_theme_section :: () { using ImGui; SeparatorText("Theme"); // ImGui wants a **u8; the registry holds Jai strings, so rebuild the array each frame in temp. items := NewArray(app_theme.entries.count, *u8,, temp); for app_theme.entries items[it_index] = to_c_string(it.name,, temp); // A copy: the index moves only once the theme has loaded, or Save would write the theme still on // screen into the file that failed. selected := app_theme.current_index; if items.count > 0 && Combo("Theme", *selected, items.data, cast(s32) items.count) { if app_theme_apply_index(selected) { app_save_user_settings_file_and_toast(tprint("theme %", app_theme.entries[selected].name)); } else { toast_show(.Error, "Could not load theme %; see the Event Log", app_theme.entries[selected].name); } } // Snapshot rather than hold the *Theme_Entry: Save and Rescan below refresh the registry, // which frees the entry names and can reallocate the array out from under us. current_name := APP_DEFAULT_THEME_NAME; current_path := ""; is_builtin := true; { entry := app_theme_current(); if entry { current_name = copy_string(entry.name,, temp); current_path = copy_string(entry.path,, temp); is_builtin = entry.source == .Builtin; } } BeginDisabled(is_builtin); if Button("Save") app_theme_save_current(); SameLine(); if Button("Reload") app_theme_reload_current(); EndDisabled(); SameLine(); if Button("Save As...") { // Seed the name box with the current theme so overwriting is one keystroke away. n := min(current_name.count, app_theme.save_as_buffer.count - 1); memcpy(app_theme.save_as_buffer.data, current_name.data, n); app_theme.save_as_buffer[n] = 0; imgui_show_modal(THEME_EDITOR_SAVE_AS_POPUP); } SameLine(); if Button("Copy") { clipboard_set_text(app_theme_to_string(*app_theme.master, *app_theme.palette, current_name, .Default)); app_theme_set_status(tprint("Copied theme % to the clipboard.", current_name), is_error = false); } ButtonTooltip("Copy the whole theme file to the clipboard."); SameLine(); if Button("Rescan") { app_theme_refresh_registry(); toast_show(.Success, "Found % themes", app_theme.entries.count); } ButtonTooltip(tprint("Re-read %", app_themes_dir())); SameLine(); if Button("Open Folder") app_theme_open_folder(); ButtonTooltip(app_themes_dir()); SameLine(); BeginDisabled(is_builtin); if Button("Open File") app_theme_open_current_file(); EndDisabled(); if !is_builtin ButtonTooltip(current_path); theme_editor_save_as_modal(); if app_theme.status.count { color := ifx app_theme.status_is_error then app_theme.palette.error else app_theme.palette.dim; TextColoredAlt(color, app_theme.status); } } theme_editor_save_as_modal :: () { using ImGui; if !BeginPopupModal(THEME_EDITOR_SAVE_AS_POPUP, null, .AlwaysAutoResize) return; defer EndPopup(); Text("Save to %", app_themes_dir()); Spacing(); SetNextItemWidth(imgui_scale(320.0)); submitted := InputText("Name", app_theme.save_as_buffer.data, cast(u64) app_theme.save_as_buffer.count, .EnterReturnsTrue); if IsWindowAppearing() SetKeyboardFocusHere(-1); Spacing(); if Button("Save") || submitted { name := to_string(app_theme.save_as_buffer.data); if app_theme_save_as(name) CloseCurrentPopup(); } SameLine(); if Button("Cancel") CloseCurrentPopup(); } // ---------------------------------------------------------------- tabs theme_editor_sizes_tab :: (style: *ImGui.Style, max_border_size: float) -> bool { using ImGui; changed := false; SeparatorText("Main"); changed |= SliderFloat2("WindowPadding", *style.WindowPadding.component, 0, 20, "%.0f"); changed |= SliderFloat2("FramePadding", *style.FramePadding.component, 0, 20, "%.0f"); changed |= SliderFloat2("ItemSpacing", *style.ItemSpacing.component, 0, 20, "%.0f"); changed |= SliderFloat2("ItemInnerSpacing", *style.ItemInnerSpacing.component, 0, 20, "%.0f"); changed |= SliderFloat2("TouchExtraPadding", *style.TouchExtraPadding.component, 0, 10, "%.0f"); changed |= SliderFloat ("IndentSpacing", *style.IndentSpacing, 0, 30, "%.0f"); changed |= SliderFloat ("GrabMinSize", *style.GrabMinSize, 1, 20, "%.0f"); SeparatorText("Borders"); changed |= SliderFloat("WindowBorderSize", *style.WindowBorderSize, 0, max_border_size, "%.0f"); changed |= SliderFloat("ChildBorderSize", *style.ChildBorderSize, 0, max_border_size, "%.0f"); changed |= SliderFloat("PopupBorderSize", *style.PopupBorderSize, 0, max_border_size, "%.0f"); changed |= SliderFloat("FrameBorderSize", *style.FrameBorderSize, 0, max_border_size, "%.0f"); changed |= SliderFloat("ActiveWindowBorderSize", *app_theme.palette.active_window_border_size, 0, 6, "%.1f"); SeparatorText("Rounding"); changed |= SliderFloat("WindowRounding", *style.WindowRounding, 0, 12, "%.0f"); changed |= SliderFloat("ChildRounding", *style.ChildRounding, 0, 12, "%.0f"); changed |= SliderFloat("FrameRounding", *style.FrameRounding, 0, 12, "%.0f"); changed |= SliderFloat("PopupRounding", *style.PopupRounding, 0, 12, "%.0f"); changed |= SliderFloat("GrabRounding", *style.GrabRounding, 0, 12, "%.0f"); SeparatorText("Scrollbar"); changed |= SliderFloat("ScrollbarSize", *style.ScrollbarSize, 1, 20, "%.0f"); changed |= SliderFloat("ScrollbarRounding", *style.ScrollbarRounding, 0, 12, "%.0f"); changed |= SliderFloat("ScrollbarPadding", *style.ScrollbarPadding, 0, 10, "%.0f"); SeparatorText("Tabs"); changed |= SliderFloat("TabBorderSize", *style.TabBorderSize, 0, max_border_size, "%.0f"); changed |= SliderFloat("TabBarBorderSize", *style.TabBarBorderSize, 0, max_border_size, "%.0f"); changed |= SliderFloat("TabBarOverlineSize", *style.TabBarOverlineSize, 0, max(3.0, max_border_size), "%.0f"); changed |= DragFloat ("TabMinWidthBase", *style.TabMinWidthBase, 0.5, 1, 500, "%.0f"); changed |= DragFloat ("TabMinWidthShrink", *style.TabMinWidthShrink, 0.5, 1, 500, "%.0f"); changed |= SliderFloat("TabRounding", *style.TabRounding, 0, 12, "%.0f"); SeparatorText("Tables"); changed |= SliderFloat2("CellPadding", *style.CellPadding.component, 0, 20, "%.0f"); changed |= SliderAngle ("TableAngledHeadersAngle", *style.TableAngledHeadersAngle, -50, 50); changed |= SliderFloat2("TableAngledHeadersTextAlign", *style.TableAngledHeadersTextAlign.component, 0, 1, "%.2f"); SeparatorText("Trees"); { // TreeLinesFlags is one of three whole values, not a free bitfield, so it gets a combo. OPTIONS :: ImGui.TreeNodeFlags.[.DrawLinesNone, .DrawLinesFull, .DrawLinesToNodes]; if BeginCombo("TreeLinesFlags", to_c_string(theme_tree_lines_name(style.TreeLinesFlags),, temp)) { for OPTIONS { if Selectable(to_c_string(theme_tree_lines_name(it),, temp), style.TreeLinesFlags == it) { style.TreeLinesFlags = it; changed = true; } } EndCombo(); } } changed |= SliderFloat("TreeLinesSize", *style.TreeLinesSize, 0, max_border_size, "%.0f"); changed |= SliderFloat("TreeLinesRounding", *style.TreeLinesRounding, 0, 12, "%.0f"); SeparatorText("Windows"); changed |= SliderFloat2("WindowTitleAlign", *style.WindowTitleAlign.component, 0, 1, "%.2f"); changed |= SliderFloat ("WindowBorderHoverPadding", *style.WindowBorderHoverPadding, 1, 20, "%.0f"); changed |= SliderFloat2("WindowMinSize", *style.WindowMinSize.component, 1, 200, "%.0f"); { // ImGuiDir_None is -1, so shift into the combo's 0-based index space and back. position := (cast(s32) style.WindowMenuButtonPosition) + 1; if Combo("WindowMenuButtonPosition", *position, "None\0Left\0Right\0") { style.WindowMenuButtonPosition = cast(ImGui.Dir)(position - 1); changed = true; } } SeparatorText("Widgets"); changed |= SliderFloat("ColorMarkerSize", *style.ColorMarkerSize, 0, 8, "%.0f"); { position := cast(s32) style.ColorButtonPosition; if Combo("ColorButtonPosition", *position, "Left\0Right\0") { style.ColorButtonPosition = cast(ImGui.Dir) position; changed = true; } } changed |= SliderFloat2("ButtonTextAlign", *style.ButtonTextAlign.component, 0, 1, "%.2f"); changed |= SliderFloat2("SelectableTextAlign", *style.SelectableTextAlign.component, 0, 1, "%.2f"); changed |= SliderFloat ("SeparatorSize", *style.SeparatorSize, 0, 10, "%.0f"); changed |= SliderFloat ("SeparatorTextBorderSize", *style.SeparatorTextBorderSize, 0, 10, "%.0f"); changed |= SliderFloat2("SeparatorTextAlign", *style.SeparatorTextAlign.component, 0, 1, "%.2f"); changed |= SliderFloat2("SeparatorTextPadding", *style.SeparatorTextPadding.component, 0, 40, "%.0f"); changed |= SliderFloat ("LogSliderDeadzone", *style.LogSliderDeadzone, 0, 12, "%.0f"); changed |= SliderFloat ("ImageRounding", *style.ImageRounding, 0, 12, "%.0f"); changed |= SliderFloat ("ImageBorderSize", *style.ImageBorderSize, 0, max_border_size, "%.0f"); SeparatorText("Docking"); changed |= Checkbox ("DockingNodeHasCloseButton", *style.DockingNodeHasCloseButton); changed |= SliderFloat("DockingSeparatorSize", *style.DockingSeparatorSize, 0, 12, "%.0f"); SeparatorText("Tooltips"); changed |= theme_editor_hover_flags("HoverFlagsForTooltipMouse", *style.HoverFlagsForTooltipMouse); changed |= theme_editor_hover_flags("HoverFlagsForTooltipNav", *style.HoverFlagsForTooltipNav); SeparatorText("Misc"); changed |= SliderFloat2("DisplayWindowPadding", *style.DisplayWindowPadding.component, 0, 30, "%.0f"); changed |= SliderFloat2("DisplaySafeAreaPadding", *style.DisplaySafeAreaPadding.component, 0, 30, "%.0f"); changed |= SliderFloat ("MouseCursorScale", *style.MouseCursorScale, 0.5, 4, "%.2f"); return changed; } theme_editor_hover_flags :: (label: string, flags: *ImGui.HoveredFlags) -> bool { using ImGui; if !TreeNodeEx(to_c_string(label,, temp)) return false; defer TreePop(); changed := false; as_int := cast(*s32) flags; changed |= CheckboxFlags("DelayNone", as_int, cast(s32) HoveredFlags.DelayNone); changed |= CheckboxFlags("DelayShort", as_int, cast(s32) HoveredFlags.DelayShort); changed |= CheckboxFlags("DelayNormal", as_int, cast(s32) HoveredFlags.DelayNormal); changed |= CheckboxFlags("Stationary", as_int, cast(s32) HoveredFlags.Stationary); changed |= CheckboxFlags("NoSharedDelay", as_int, cast(s32) HoveredFlags.NoSharedDelay); return changed; } theme_tree_lines_name :: (flags: ImGui.TreeNodeFlags) -> string { if flags == .DrawLinesNone return "DrawLinesNone"; if flags == .DrawLinesFull return "DrawLinesFull"; if flags == .DrawLinesToNodes return "DrawLinesToNodes"; return "(custom)"; } theme_editor_colors_tab :: (style: *ImGui.Style, ref: *ImGui.Style, live: *ImGui.Style) -> bool { using ImGui; changed := false; app_theme.color_filter.Draw(*app_theme.color_filter, "Filter colors", GetFontSize() * 16); if RadioButton("Opaque", app_theme.alpha_flags == .AlphaOpaque) app_theme.alpha_flags = .AlphaOpaque; SameLine(); if RadioButton("Alpha", app_theme.alpha_flags == .None) app_theme.alpha_flags = .None; SameLine(); if RadioButton("Both", app_theme.alpha_flags == .AlphaPreviewHalf) app_theme.alpha_flags = .AlphaPreviewHalf; SameLine(); ButtonTooltip("Left-click a color square to open the picker, right-click for edit options."); SetNextWindowSizeConstraints(ImVec2.{0, GetTextLineHeightWithSpacing() * 10}, ImVec2.{FLOAT32_MAX, FLOAT32_MAX}); BeginChild("##colors", ImVec2.{0, 0}, .Borders | .NavFlattened, .AlwaysVerticalScrollbar); { defer EndChild(); // BeginChild must be paired whatever it returned PushItemWidth(GetFontSize() * -12); // negative: window width minus 12em, leaving room on the right defer PopItemWidth(); for i: 0 .. (cast(s32) Col.COUNT) - 1 { name := to_string(GetStyleColorName(cast(Col) i)); if !app_theme.color_filter.PassFilter(*app_theme.color_filter, name) continue; PushID(i); defer PopID(); if Button("?") DebugFlashStyleColor(cast(Col) i); SetItemTooltip("Flash this color wherever it is used."); SameLine(); changed |= ColorEdit4("##color", *style.Colors[i].component, .AlphaBar | app_theme.alpha_flags); if style.Colors[i] != ref.Colors[i] { SameLine(0, live.ItemInnerSpacing.x); if Button("Save") ref.Colors[i] = style.Colors[i]; SameLine(0, live.ItemInnerSpacing.x); if Button("Revert") { style.Colors[i] = ref.Colors[i]; changed = true; } } SameLine(0, live.ItemInnerSpacing.x); TextUnformatted(name); } } return changed; } theme_editor_palette_tab :: () -> bool { using ImGui; changed := false; TextWrapped("Colors the app draws with that have no ImGui style slot. Ported themes fill these from the source theme's editor colors."); Spacing(); pal := *app_theme.palette; for member: theme_palette_fields() { color := cast(*Vector4) (cast(*u8) pal + member.offset_in_bytes); PushID(cast(s32) it_index); defer PopID(); changed |= ColorEdit4(to_c_string(member.name,, temp), *color.component, .AlphaBar | app_theme.alpha_flags); } return changed; } theme_editor_rendering_tab :: (style: *ImGui.Style) -> bool { using ImGui; changed := false; changed |= Checkbox("Anti-aliased lines", *style.AntiAliasedLines); changed |= Checkbox("Anti-aliased lines use texture", *style.AntiAliasedLinesUseTex); changed |= Checkbox("Anti-aliased fill", *style.AntiAliasedFill); PushItemWidth(GetFontSize() * 8); defer PopItemWidth(); if DragFloat("Curve Tessellation Tolerance", *style.CurveTessellationTol, 0.02, 0.1, 10, "%.2f") { if style.CurveTessellationTol < 0.1 style.CurveTessellationTol = 0.1; changed = true; } changed |= DragFloat("Circle Tessellation Max Error", *style.CircleTessellationMaxError, 0.005, 0.1, 5, "%.2f", .AlwaysClamp); // 0.0 is deliberately out of range: it would make the whole UI invisible with no way back. changed |= DragFloat("Global Alpha", *style.Alpha, 0.005, 0.2, 1, "%.2f"); changed |= DragFloat("Disabled Alpha", *style.DisabledAlpha, 0.005, 0, 1, "%.2f"); return changed; }