437 lines
18 KiB
Plaintext
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;
|
|
}
|