mcap/src/ui/imgui_theme_editor.jai

437 lines
18 KiB
Plaintext

// 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;
}