From 2bf595eda689b6d0716612d224a8b3bb5f54cf68 Mon Sep 17 00:00:00 2001 From: Patitotective Date: Tue, 24 Oct 2023 21:39:11 -0500 Subject: [PATCH] WIP --- ImExample.nimble | 17 +-- main.nim | 101 +++++-------- resources.nim | 14 +- src/settingsmodal.nim | 337 +++++++++++++++++++++--------------------- src/types.nim | 153 +++++++++++++------ src/utils.nim | 22 +-- 6 files changed, 342 insertions(+), 302 deletions(-) diff --git a/ImExample.nimble b/ImExample.nimble index 61d81af..9e766c1 100644 --- a/ImExample.nimble +++ b/ImExample.nimble @@ -8,19 +8,18 @@ backend = "cpp" # Dependencies requires "nim >= 1.6.2" -requires "kdl >= 1.0.0" +requires "kdl >= 1.2.4" requires "nimgl >= 1.3.2" requires "stb_image >= 2.5" requires "imstyle >= 1.0.0" requires "openurl >= 2.0.3" requires "tinydialogs >= 1.0.0" +requires "constructor >= 1.1.4" import std/[strformat, options] import src/types -import kdl -const configPath {.strdefine.} = "config.kdl" -const config = parseKdlFile(configPath).decode(Config) +const config = initConfig() version = config.version namedBin["main"] = config.name @@ -59,12 +58,12 @@ task buildapp, "Build the AppImage": # Make desktop file writeFile( - &"AppDir/{config.name}.desktop", + &"AppDir/{config.name}.desktop", desktop % [ - "name", config.name, - "categories", config.categories.join(";"), - "version", config.version, - "comment", config.comment, + "name", config.name, + "categories", config.categories.join(";"), + "version", config.version, + "comment", config.comment, "arch", arch ] ) diff --git a/main.nim b/main.nim index 9a53a00..f04058d 100644 --- a/main.nim +++ b/main.nim @@ -11,11 +11,8 @@ import src/[settingsmodal, utils, types, icons] when defined(release): import resources -# TODO make configuration or at least settings inside the code and nto in the configuration file # TODO be able to reset single preferences to their default value -# const configPath {.strdefine.} = "config.kdl" - proc getConfigDir(app: App): string = getConfigDir() / app.config.name @@ -79,7 +76,7 @@ proc drawMainMenuBar(app: var App) = if igBeginMenu("Edit"): if igMenuItem("Hello"): - echo "Hello" + discard messageBox(app.config.name, "Hello, earthling", DialogType.Ok, IconType.Info, Button.Yes) igEndMenu() @@ -95,7 +92,8 @@ proc drawMainMenuBar(app: var App) = # See https://github.com/ocornut/imgui/issues/331#issuecomment-751372071 if openPrefs: - app.settingsmodal.cache = app.prefs[settings] + echo "TODO" + # app.settingsmodal.cache = app.prefs[settings] igOpenPopup("Settings") if openAbout: igOpenPopup("###about") @@ -120,7 +118,7 @@ proc drawMain(app: var App) = # Draw the main window app.fonts[1].igPushFont() igText("Unicode fonts (NotoSansJP-Regular.otf)") - igText("日本語がいいですね。 " & FA_SmileO) + igText("日本語の言葉 " & FA_SmileO) igPopFont() igEnd() @@ -163,9 +161,10 @@ proc initWindow(app: var App) = glfwWindowHint(GLFWMaximized, GLFW_TRUE) app.win = glfwCreateWindow( - app.prefs[winsize].x, - app.prefs[winsize].y, + app.prefs[winsize].w, + app.prefs[winsize].h, cstring app.config.name, + # glfwGetPrimaryMonitor(), # Show the window on the primary monitor icon = false # Do not use default icon ) @@ -176,16 +175,16 @@ proc initWindow(app: var App) = var icon = initGLFWImage(app.res(app.config.iconPath).readImageFromMemory()) app.win.setWindowIcon(1, icon.addr) - if app.config.minSize.isSome: - app.win.setWindowSizeLimits(app.config.minSize.get.x, app.config.minSize.get.y, GLFW_DONT_CARE, GLFW_DONT_CARE) # minWidth, minHeight, maxWidth, maxHeight + # min width, min height, max widht, max height + app.win.setWindowSizeLimits(app.config.minSize.w, app.config.minSize.h, GLFW_DONT_CARE, GLFW_DONT_CARE) # If negative pos, center the window in the first monitor if app.prefs[winpos].x < 0 or app.prefs[winpos].y < 0: var monitorX, monitorY, count, width, height: int32 - let monitors = glfwGetMonitors(count.addr) - let videoMode = monitors[0].getVideoMode() + let monitor = glfwGetMonitors(count.addr)[0]#glfwGetPrimaryMonitor() + let videoMode = monitor.getVideoMode() - monitors[0].getMonitorPos(monitorX.addr, monitorY.addr) + monitor.getMonitorPos(monitorX.addr, monitorY.addr) app.win.getWindowSize(width.addr, height.addr) app.win.setWindowPos( monitorX + int32((videoMode.width - width) / 2), @@ -194,66 +193,43 @@ proc initWindow(app: var App) = else: app.win.setWindowPos(app.prefs[winpos].x, app.prefs[winpos].y) -proc initPrefs(app: var App) = - app.prefs = initKPrefs( - path = (app.getConfigDir() / "prefs").changeFileExt("kdl"), - default = Prefs( - maximized: false, - winpos: (-1i32, -1i32), # Negative numbers center the window - winsize: (600i32, 650i32), - settings: Settings( - input: "Hello World", - hintInput: "", - checkbox: true, - combo: Abc.C, - radio: Abc.A, - numbers: Numbers( - slider: 4, - floatSlider: 2.5, - spin: 4, - floatSpin: 3.14, - ), - colors: Colors( - rgb: (1.0f, 0.0f, 0.2f), - rgba: (0.4f, 0.7f, 0.0f, 0.5f), - ), - ) - ) - ) - -proc checkSettings(app: App) = - for name, field in app.settingsmodal.cache.fieldPairs: - var found = false - for key, _ in app.config.settings: - if key.eqIdent name: - found = true - break - - if not found: - raise newException(KeyError, name & " is not in defined in the config file") - proc initApp(): App = when defined(release): result.resources = readResources() - result.checkSettings() + result.config = initConfig() + + let filename = + when defined(release): "prefs" + else: "prefs_dev" - result.initPrefs() + result.prefs = initKPrefs( + path = (result.getConfigDir() / filename).changeFileExt("kdl"), + default = initPrefs() + ) template initFonts(app: var App) = # Merge ForkAwesome icon font let config = utils.newImFontConfig(mergeMode = true) - let ranges = [uint16 FA_Min, uint16 FA_Max] + let iconFontGlyphRanges = [uint16 FA_Min, uint16 FA_Max] - for e, (path, size) in app.config.fonts.fonts: + for e, font in app.config.fonts: let glyph_ranges = - case e - of 1: io.fonts.getGlyphRangesJapanese() - else: nil - - app.fonts[e] = io.fonts.igAddFontFromMemoryTTF(app.res(path), size, glyph_ranges = glyph_ranges) - if app.config.fonts.iconFontPath.len > 0: - io.fonts.igAddFontFromMemoryTTF(app.res(app.config.fonts.iconFontPath), size, config.unsafeAddr, ranges[0].unsafeAddr) + case font.glyphRanges + of GlyphRanges.Default: io.fonts.getGlyphRangesDefault() + of ChineseFull: io.fonts.getGlyphRangesChineseFull() + of ChineseSimplified: io.fonts.getGlyphRangesChineseSimplifiedCommon() + of Cyrillic: io.fonts.getGlyphRangesCyrillic() + of Japanese: io.fonts.getGlyphRangesJapanese() + of Korean: io.fonts.getGlyphRangesKorean() + of Thai: io.fonts.getGlyphRangesThai() + of Vietnamese: io.fonts.getGlyphRangesVietnamese() + + app.fonts[e] = io.fonts.igAddFontFromMemoryTTF(app.res(font.path), font.size, glyph_ranges = glyph_ranges) + + # Here we add the icon font to every font + if app.config.iconFontPath.len > 0: + io.fonts.igAddFontFromMemoryTTF(app.res(app.config.iconFontPath), font.size, config.unsafeAddr, iconFontGlyphRanges[0].unsafeAddr) proc terminate(app: var App) = var x, y, width, height: int32 @@ -309,3 +285,4 @@ proc main() = when isMainModule: main() + diff --git a/resources.nim b/resources.nim index c4301ce..7c48bb8 100644 --- a/resources.nim +++ b/resources.nim @@ -2,16 +2,14 @@ import std/[sequtils, tables] import kdl import src/types -const configPath {.strdefine.} = "config.kdl" -const config = parseKdlFile(configPath).decode(Config) +const config = initConfig() const resourcesPaths = @[ - configPath, - config.stylePath, - config.iconPath, - config.fonts.iconFontPath, -] & config.fonts.fonts.mapIt(it.path) + config.stylePath, + config.iconPath, + config.iconFontPath, +] & config.fonts.mapIt(it.path) -proc readResources*(): Table[string, string] {.compileTime.} = +proc readResources*(): Table[string, string] {.compileTime.} = for path in resourcesPaths: result[path] = slurp(path) diff --git a/src/settingsmodal.nim b/src/settingsmodal.nim index 918ed13..d766880 100644 --- a/src/settingsmodal.nim +++ b/src/settingsmodal.nim @@ -6,184 +6,179 @@ import nimgl/imgui import utils, icons, types -proc drawSettings(app: var App) = - inputText("Input", app.settings.input) - - return - for name, field in settings.fieldPairs: - var key: string +proc settingLabel(name: string, setting: Setting[auto]): cstring = + cstring (if setting.display.len == 0: name else: setting.display) & ": " - for k in settingsConfig.keys: - if k.eqIdent name: - key = k - break - - let data = settingsConfig[key] - let label = cstring (if data.display.len > 0: data.display else: key.capitalizeAscii()) & ": " +proc drawSettings(app: var App) = + for name, setting in app.prefs[settings].fieldPairs: + let label = settingLabel(name, setting) let id = cstring "##" & name - if data.kind != stSection: + if setting.kind != stSection: igText(label); igSameLine(0, 0) - igDummy(igVec2(maxLabelWidth - igCalcTextSize(label).x, 0)) + igDummy(igVec2(app.maxLabelWidth - igCalcTextSize(label).x, 0)) igSameLine(0, 0) - case data.kind + case setting.kind of stInput: - assert field is string - when field is string: - let flags = parseMakeFlags[ImGuiInputTextFlags](data.flags) - let buffer = newString(int data.maxbuf, field) - - if data.hint.isSome: - if igInputTextWithHint(id, cstring data.hint.get, cstring buffer, data.maxbuf, flags): - field = buffer.cleanString() - else: - if igInputText(id, cstring buffer, data.maxbuf, flags): - field = buffer.cleanString() - of stCheck: - assert field is bool - when field is bool: - igCheckbox(id, field.addr) - of stSlider: - assert field is int32 - assert data.min.isSome and data.max.isSome - when field is int32: - igSliderInt( - id, - field.addr, - int32 data.min.get, - int32 data.max.get, - cstring (if data.format.isSome: data.format.get else: "%d"), - parseMakeFlags[ImGuiSliderFlags](data.flags) - ) - of stFSlider: - assert field is float32 - assert data.min.isSome and data.max.isSome - when field is float32: - igSliderFloat( - id, - field.addr, - data.min.get, - data.max.get, - cstring (if data.format.isSome: data.format.get else: "%.3f"), - parseMakeFlags[ImGuiSliderFlags](data.flags) - ) - of stSpin: - assert field is int32 - when field is int32: - var temp = field - if igInputInt( - id, - temp.addr, - int32 data.step, - int32 data.stepfast, - parseMakeFlags[ImGuiInputTextFlags](data.flags) - ) and (data.min.isNone or temp >= int32(data.min.get)) and (data.max.isNone or temp <= int32(data.max.get)): - field = temp - of stFSpin: - assert field is float32 - when field is float32: - var temp = field - if igInputFloat( - id, - temp.addr, - data.step, - data.stepfast, - cstring (if data.format.isSome: data.format.get else: "%.3f"), - parseMakeFlags[ImGuiInputTextFlags](data.flags) - ) and (data.min.isNone or temp >= data.min.get) and (data.max.isNone or temp <= data.max.get): - field = temp - of stCombo: - assert field is enum - when field is enum: - if igBeginCombo(id, cstring $field, parseMakeFlags[ImGuiComboFlags](data.flags)): - for item in data.items: - let itenum = parseEnum[typeof field](item) - if igSelectable(cstring item, field == itenum): - field = itenum - - igEndCombo() - of stRadio: - assert field is enum - when field is enum: - for e, item in data.items: - let itenum = parseEnum[typeof field](item) - if igRadioButton(cstring $itenum & "##" & name & $e, itenum == field): - field = itenum - - if e < data.items.high: - igSameLine() - of stRGB: - assert field is tuple[r, g, b: float32] - when field is tuple[r, g, b: float32]: - var colArray = [field.r, field.g, field.b] - if igColorEdit3(id, colArray, parseMakeFlags[ImGuiColorEditFlags](data.flags)): - field = (colArray[0], colArray[1], colArray[2]) - of stRGBA: - assert field is tuple[r, g, b, a: float32] - when field is tuple[r, g, b, a: float32]: - var colArray = [field.r, field.g, field.b, field.a] - if igColorEdit4(id, colArray, parseMakeFlags[ImGuiColorEditFlags](data.flags)): - field = (colArray[0], colArray[1], colArray[2], colArray[3]) - of stFile: - assert field is string - when field is string: - igPushID(id) - igInputTextWithHint(id, "Nothing selected", cstring field, uint field.len, flags = ImGuiInputTextFlags.ReadOnly) - igSameLine() - if (igIsItemHovered(flags = AllowWhenDisabled) and igIsMouseDoubleClicked(ImGuiMouseButton.Left)) or igButton("Browse " & FA_FolderOpen): - if (let path = openFileDialog("Choose File", getCurrentDir() / "\0", data.filterPatterns, data.singleFilterDescription); path.len > 0): - field = path - igPopID() - of stFiles: - assert field is seq[string] - when field is seq[string]: - let str = field.join(",") - igPushID(id) - igInputTextWithHint(id, "Nothing selected", cstring str, uint str.len, flags = ImGuiInputTextFlags.ReadOnly) - igSameLine() - if (igIsItemHovered(flags = AllowWhenDisabled) and igIsMouseDoubleClicked(ImGuiMouseButton.Left)) or igButton("Browse " & FA_FolderOpen): - if (let paths = openMultipleFilesDialog("Choose Files", getCurrentDir() / "\0", data.filterPatterns, data.singleFilterDescription); paths.len > 0): - field = paths - igPopID() - of stFolder: - assert field is string - when field is string: - igPushID(id) - igInputTextWithHint(id, "Nothing selected", cstring field, uint field.len, flags = ImGuiInputTextFlags.ReadOnly) - igSameLine() - if (igIsItemHovered(flags = AllowWhenDisabled) and igIsMouseDoubleClicked(ImGuiMouseButton.Left)) or igButton("Browse " & FA_FolderOpen): - if (let path = selectFolderDialog("Choose Folder", getCurrentDir() / "\0"); path.len > 0): - field = path - igPopID() - of stSection: - assert field is object - when field is object: - igPushID(id) - if igCollapsingHeader(label, parseMakeFlags[ImGuiTreeNodeFlags](data.flags)): - igIndent() - drawSettings(field, data.content, maxLabelWidth) - igUnindent() - igPopID() - - if data.help.len > 0: - igSameLine() - igHelpMarker(data.help) - -proc calcMaxLabelWidth(settings: OrderedTable[string, Setting]): float32 = - for name, data in settings: - let label = cstring (if data.display.len > 0: data.display else: name.capitalizeAscii()) & ": " + let flags = makeFlags(setting.inputFlags) + let buffer = newString(100, setting.inputCache) - if (let width = ( - if data.kind == stSection: - calcMaxLabelWidth(data.content) + if setting.hint.len > 0: + if igInputTextWithHint(id, cstring setting.hint, cstring buffer, 100, flags): + setting.inputCache = buffer.cleanString() + else: + if igInputText(id, cstring buffer, 100, flags): + setting.inputCache = buffer.cleanString() + else: discard + # of stCheck: + # assert field is bool + # when field is bool: + # igCheckbox(id, field.addr) + # of stSlider: + # assert field is int32 + # assert setting.min.isSome and setting.max.isSome + # when field is int32: + # igSliderInt( + # id, + # field.addr, + # int32 setting.min.get, + # int32 setting.max.get, + # cstring (if setting.format.isSome: setting.format.get else: "%d"), + # parseMakeFlags[ImGuiSliderFlags](setting.flags) + # ) + # of stFSlider: + # assert field is float32 + # assert setting.min.isSome and setting.max.isSome + # when field is float32: + # igSliderFloat( + # id, + # field.addr, + # setting.min.get, + # setting.max.get, + # cstring (if setting.format.isSome: setting.format.get else: "%.3f"), + # parseMakeFlags[ImGuiSliderFlags](setting.flags) + # ) + # of stSpin: + # assert field is int32 + # when field is int32: + # var temp = field + # if igInputInt( + # id, + # temp.addr, + # int32 setting.step, + # int32 setting.stepfast, + # parseMakeFlags[ImGuiInputTextFlags](setting.flags) + # ) and (setting.min.isNone or temp >= int32(setting.min.get)) and (setting.max.isNone or temp <= int32(setting.max.get)): + # field = temp + # of stFSpin: + # assert field is float32 + # when field is float32: + # var temp = field + # if igInputFloat( + # id, + # temp.addr, + # setting.step, + # setting.stepfast, + # cstring (if setting.format.isSome: setting.format.get else: "%.3f"), + # parseMakeFlags[ImGuiInputTextFlags](setting.flags) + # ) and (setting.min.isNone or temp >= setting.min.get) and (setting.max.isNone or temp <= setting.max.get): + # field = temp + # of stCombo: + # assert field is enum + # when field is enum: + # if igBeginCombo(id, cstring $field, parseMakeFlags[ImGuiComboFlags](setting.flags)): + # for item in setting.items: + # let itenum = parseEnum[typeof field](item) + # if igSelectable(cstring item, field == itenum): + # field = itenum + + # igEndCombo() + # of stRadio: + # assert field is enum + # when field is enum: + # for e, item in setting.items: + # let itenum = parseEnum[typeof field](item) + # if igRadioButton(cstring $itenum & "##" & name & $e, itenum == field): + # field = itenum + + # if e < setting.items.high: + # igSameLine() + # of stRGB: + # assert field is tuple[r, g, b: float32] + # when field is tuple[r, g, b: float32]: + # var colArray = [field.r, field.g, field.b] + # if igColorEdit3(id, colArray, parseMakeFlags[ImGuiColorEditFlags](setting.flags)): + # field = (colArray[0], colArray[1], colArray[2]) + # of stRGBA: + # assert field is tuple[r, g, b, a: float32] + # when field is tuple[r, g, b, a: float32]: + # var colArray = [field.r, field.g, field.b, field.a] + # if igColorEdit4(id, colArray, parseMakeFlags[ImGuiColorEditFlags](setting.flags)): + # field = (colArray[0], colArray[1], colArray[2], colArray[3]) + # of stFile: + # assert field is string + # when field is string: + # igPushID(id) + # igInputTextWithHint(id, "Nothing selected", cstring field, uint field.len, flags = ImGuiInputTextFlags.ReadOnly) + # igSameLine() + # if (igIsItemHovered(flags = AllowWhenDisabled) and igIsMouseDoubleClicked(ImGuiMouseButton.Left)) or igButton("Browse " & FA_FolderOpen): + # if (let path = openFileDialog("Choose File", getCurrentDir() / "\0", setting.filterPatterns, setting.singleFilterDescription); path.len > 0): + # field = path + # igPopID() + # of stFiles: + # assert field is seq[string] + # when field is seq[string]: + # let str = field.join(",") + # igPushID(id) + # igInputTextWithHint(id, "Nothing selected", cstring str, uint str.len, flags = ImGuiInputTextFlags.ReadOnly) + # igSameLine() + # if (igIsItemHovered(flags = AllowWhenDisabled) and igIsMouseDoubleClicked(ImGuiMouseButton.Left)) or igButton("Browse " & FA_FolderOpen): + # if (let paths = openMultipleFilesDialog("Choose Files", getCurrentDir() / "\0", setting.filterPatterns, setting.singleFilterDescription); paths.len > 0): + # field = paths + # igPopID() + # of stFolder: + # assert field is string + # when field is string: + # igPushID(id) + # igInputTextWithHint(id, "Nothing selected", cstring field, uint field.len, flags = ImGuiInputTextFlags.ReadOnly) + # igSameLine() + # if (igIsItemHovered(flags = AllowWhenDisabled) and igIsMouseDoubleClicked(ImGuiMouseButton.Left)) or igButton("Browse " & FA_FolderOpen): + # if (let path = selectFolderDialog("Choose Folder", getCurrentDir() / "\0"); path.len > 0): + # field = path + # igPopID() + # of stSection: + # assert field is object + # when field is object: + # igPushID(id) + # if igCollapsingHeader(label, parseMakeFlags[ImGuiTreeNodeFlags](setting.flags)): + # igIndent() + # drawSettings(field, setting.content, maxLabelWidth) + # igUnindent() + # igPopID() + + if setting.help.len > 0: + igSameLine() + igHelpMarker(setting.help) + +proc calcMaxLabelWidth(settings: auto): float32 = + when settings is object: + for name, setting in settings.fieldPairs: + when setting is Setting: + let label = settingLabel(name, setting) + + let width = + if setting.kind == stSection: + calcMaxLabelWidth(setting.content) + else: + igCalcTextSize(label).x + if width > result: + result = width else: - igCalcTextSize(label).x - ); width > result): - result = width + {.error: name & "is not a settings object".} proc drawSettingsmodal*(app: var App) = - if app.settingsmodal.maxLabelWidth <= 0: - app.settingsmodal.maxLabelWidth = app.config.settings.calcMaxLabelWidth() + if app.maxLabelWidth <= 0: + app.maxLabelWidth = app.prefs[settings].calcMaxLabelWidth() igSetNextWindowPos(igGetMainViewport().getCenter(), Always, igVec2(0.5f, 0.5f)) @@ -196,13 +191,15 @@ proc drawSettingsmodal*(app: var App) = igSpacing() if igButton("Save"): - app.prefs.content.settings = app.settingsmodal.cache + echo "TODO: save" + # app.prefs[settings] = app.settingsmodal.cache igCloseCurrentPopup() igSameLine() if igButton("Cancel"): - app.settingsmodal.cache = app.prefs[settings] + echo "TODO: cache" + # app.settingsmodal.cache = app.prefs[settings] igCloseCurrentPopup() igSameLine() diff --git a/src/types.nim b/src/types.nim index 9658d33..8d25109 100644 --- a/src/types.nim +++ b/src/types.nim @@ -1,10 +1,10 @@ -import std/[strformat, strutils, tables] +import std/[tables] -import constructor/defaults import nimgl/[imgui, glfw] +import constructor/defaults import kdl, kdl/types -type # Config +type SettingType* = enum stInput # Input text stCheck # Checkbox @@ -21,27 +21,35 @@ type # Config stFiles # Multiple files picker stFolder # Folder picker - RGB* = tuple[r, g, b: range[0f..1f]] - RGBA* = tuple[r, g, b, a: range[0f..1f]] + RGB* = object + r*, g*, b*: range[0f..1f] + + RGBA* = object + r*, g*, b*, a*: range[0f..1f] + + Empty* = object # https://forum.nim-lang.org/t/10565 # Because branches cannot have shared and additional fields right now (https://github.com/nim-lang/RFCs/issues/368) # There are some weird field names in the object below # S is the object for a section - Setting*[S: object or void] = object + Setting*[T: object or enum or bool] = object display*: string help*: string case kind*: SettingType of stInput: inputVal*, inputDefault*, inputCache*: string inputFlags*: seq[ImGuiInputTextFlags] - maxLength*: Option[uint] + limits*: HSlice[uint, Option[uint]] hint*: string - of stCombo, stRadio: - comboRadioVal*, comboRadioDefault*, comboRadioCache*: string + of stCombo: + comboVal*, comboDefault*, comboCache*: T comboFlags*: seq[ImGuiComboFlags] - items*: seq[string] + comboIncludeOnly*: seq[T] + of stRadio: + radioVal*, radioDefault*, radioCache*: T + radioIncludeOnly*: seq[T] of stSection: - content*: S + content*: T sectionFlags*: seq[ImGuiTreeNodeFlags] of stSlider: sliderVal*, sliderDefault*, sliderCache*: int32 @@ -83,27 +91,93 @@ type # Config rgbaVal*, rgbaDefault*, rgbaCache*: RGBA rgbaFlags*: seq[ImGuiColorEditFlags] -proc inputSetting(display, help = "", default = "", hint = "", maxLength = uint.none, flags = newSeq[ImGuiInputTextFlags]()): Setting[void] = - ## If maxLength is none, the buffer size will be increased if the buffer also increases. - Setting[void](display: display, help: help, kind: stInput, inputDefault: default, hint: hint, maxLength: maxLength, inputFlags: flags) +proc inputSetting(display, help, default, hint = "", limits = 0u..uint.none, flags = newSeq[ImGuiInputTextFlags]()): Setting[Empty] = + Setting[Empty](display: display, help: help, kind: stInput, inputDefault: default, hint: hint, limits: limits, inputFlags: flags) + +proc checkSetting(display, help = "", default: bool): Setting[Empty] = + Setting[Empty](display: display, help: help, kind: stCheck, checkDefault: default) + +proc comboSetting[T: enum](display, help = "", default: T, includeOnly = newSeq[T](), flags = newSeq[ImGuiComboFlags]()): Setting[T] = + Setting[T](display: display, help: help, kind: stCombo, comboincludeOnly: includeOnly, comboDefault: default, comboFlags: flags) + +proc radioSetting[T: enum](display, help = "", default: T, includeOnly = newSeq[T]()): Setting[T] = + Setting[T](display: display, help: help, kind: stRadio, radioincludeOnly: includeOnly, radioDefault: default) + +proc sectionSetting[T: object](display, help = "", content: T, flags = newSeq[ImGuiTreeNodeFlags]()): Setting[T] = + Setting[T](display: display, help: help, kind: stSection, content: content, sectionFlags: flags) + +proc sliderSetting(display, help = "", default = 0i32, range: Slice[int32], format = "%d", flags = newSeq[ImGuiSliderFlags]()): Setting[Empty] = + Setting[Empty](display: display, help: help, kind: stSlider, sliderDefault: default, sliderRange: range, sliderFormat: format, sliderFlags: flags) + +proc fsliderSetting(display, help = "", default = 0f, range: Slice[float32], format = "%.2f", flags = newSeq[ImGuiSliderFlags]()): Setting[Empty] = + Setting[Empty](display: display, help: help, kind: stFSlider, fsliderDefault: default, fsliderRange: range, fsliderFormat: format, fsliderFlags: flags) + +proc spinSetting(display, help = "", default = 0i32, range: Slice[int32], step = 1i32, stepFast = 10i32, flags = newSeq[ImGuiInputTextFlags]()): Setting[Empty] = + Setting[Empty](display: display, help: help, kind: stSpin, spinDefault: default, spinRange: range, step: step, stepFast: stepFast, spinFlags: flags) + +proc fspinSetting(display, help = "", default = 0f, range: Slice[float32], step = 0.1f, stepFast = 1f, format = "%.2f", flags = newSeq[ImGuiInputTextFlags]()): Setting[Empty] = + Setting[Empty](display: display, help: help, kind: stFSpin, fspinDefault: default, fspinRange: range, fstep: step, fstepFast: stepFast, fspinFormat: format, fspinFlags: flags) + +proc fileSetting(display, help, default = "", filterPatterns = newSeq[string](), singleFilterDescription = ""): Setting[Empty] = + Setting[Empty](display: display, help: help, kind: stFile, fileDefault: default, fileFilterPatterns: filterPatterns, fileSingleFilterDescription: singleFilterDescription) -proc checkSetting(display, help = "", default: bool): Setting[void] = - Setting[void](display: display, help: help, kind: stCheck, checkDefault: default) +proc filesSetting(display, help = "", default = newSeq[string](), filterPatterns = newSeq[string](), singleFilterDescription = ""): Setting[Empty] = + Setting[Empty](display: display, help: help, kind: stFiles, filesDefault: default, filesFilterPatterns: filterPatterns, filesSingleFilterDescription: singleFilterDescription) -proc comboSetting(display, help = "", items: seq[string], default: string, flags = newSeq[ImGuiComboFlags]()): Setting[void] = - Setting[void](display: display, help: help, kind: stCombo, items: items, comboRadioDefault: default, comboFlags: flags) +proc folderSetting(display, help, default = ""): Setting[Empty] = + Setting[Empty](display: display, help: help, kind: stFolder, folderDefault: default) + +proc rgbSetting(display, help = "", default: RGB, flags = newSeq[ImGuiColorEditFlags]()): Setting[Empty] = + Setting[Empty](display: display, help: help, kind: stRGB, rgbDefault: default, rgbFlags: flags) + +proc rgbaSetting(display, help = "", default: RGBA, flags = newSeq[ImGuiColorEditFlags]()): Setting[Empty] = + Setting[Empty](display: display, help: help, kind: stRGBA, rgbaDefault: default, rgbaFlags: flags) + +proc rgb*(r, g, b: float32): RGB = RGB(r: r, g: g, b: b) +proc rgba*(r, g, b, a: float32): RGBA = RGBA(r: r, g: g, b: b, a: a) type - Settings* {.defaults.} = object - a* = inputSetting(display = "Text Input") - b* = inputSetting(display = "Text Input With Hint", help = "Maximum 10 characters", hint = "type something", maxLength = 10u.some) - c* = checkSetting(display = "Checkbox", default = false) - d* = comboSetting(display = "Combo", items = @["a", "b", "c"], default = "a") + Os* {.defaults: {}.} = object + file* = fileSetting(display = "Text File", filterPatterns = @["*.txt", "*.nim", "*.kdl", "*.json"]) + files* = filesSetting(display = "Multiple files", singleFilterDescription = "Anything") + folder* = folderSetting(display = "Folder") + + Numbers* {.defaults: {}.} = object + spin* = spinSetting(display = "Int Spinner", default = 4, range = 0i32..10i32) + fspin* = fspinSetting(display = "Float Spinner", default = 3.14, range = 0f..10f) + slider* = sliderSetting(display = "Int Slider", default = 40, range = -100i32..100i32) + fslider* = fsliderSetting(display = "Float Slider", default = -2.5, range = -10f..10f) + + Colors* {.defaults: {}.} = object + rgb* = rgbSetting(default = rgb(1, 0, 0.2)) + rgba* = rgbaSetting(default = rgba(0.4, 0.7, 0, 0.5), flags = @[AlphaBar, AlphaPreviewHalf]) + + Sizes* = enum + None, Huge, Big, Medium, Small, Mini + + Settings* {.defaults: {}.} = object + input* = inputSetting(display = "Input", default = "Hello World") + input2* = inputSetting(display = "Custom Input", help = "Has a hint, 1 character minimum and 10 characters maximum and only accepts on return", hint = "Type...", limits = 1u..10u.some, flags = @[ImGuiInputTextFlags.EnterReturnsTrue]) + check* = checkSetting(display = "Checkbox", default = true) + combo* = comboSetting[Sizes](display = "Combo box", default = None) + radio* = radioSetting[Sizes](display = "Radio button", includeOnly = @[Big, Medium, Small], default = Medium) + os* = sectionSetting(display = "File dialogs", content = initOs()) + numbers* = sectionSetting(display = "Spinners and sliders", content = initNumbers()) + colors* = sectionSetting(display = "Color pickers", content = initColors()) -implDefaults(Settings) + GlyphRanges* = enum + Default, ChineseFull, ChineseSimplified, Cyrillic, Japanese, Korean, Thai, Vietnamese + + Font* = object + path*: string + size*: float32 + glyphRanges*: GlyphRanges + +proc font*(path: string, size: float32, glyphRanges = GlyphRanges.Default): Font = + Font(path: path, size: size, glyphRanges: glyphRanges) type - Config* {.defaults.} = object + Config* {.defaults: {defExported}.} = object name* = "ImExample" comment* = "ImExample is a simple Dear ImGui application example" version* = "2.0.0" @@ -123,38 +197,33 @@ type svgIconPath* = "assets/icon.svg" iconFontPath* = "assets/forkawesome-webfont.ttf" - fonts* = [ # [path, size] - (path: "assets/ProggyVector Regular.ttf", size: 16f), # Other options are Roboto-Regular.ttf, Cousine-Regular.ttf or Karla-Regular.ttf - ("assets/NotoSansJP-Regular.otf", 16f), + fonts* = [ + font("assets/ProggyVector Regular.ttf", 16f), # Other options are Roboto-Regular.ttf, Cousine-Regular.ttf or Karla-Regular.ttf + font("assets/NotoSansJP-Regular.otf", 16f, GlyphRanges.Japanese), ] # AppImage ghRepo* = ["Patitotective", "ImTemplate"] # [username, repository] # Window - minSize* = [200, 200] # [width, height] - -type - Prefs* = object - maximized*: bool - winpos*: tuple[x, y: int32] - winsize*: tuple[x, y: int32] - settings*: Settings + minSize* = (w: 200i32, h: 200i32) # < 0: don't care - SettingsModal* = object - maxLabelWidth*: float32 + Prefs* {.defaults: {defExported}.} = object + maximized* = false + winpos* = (x: -1i32, y: -1i32) # < 0: center the window + winsize* = (w: 600i32, h: 650i32) + settings* = initSettings() App* = object win*: GLFWWindow config*: Config prefs*: KdlPrefs[Prefs] # These are the values that will be saved in the config file - fonts*: array[2, ptr ImFont] - settingsmodal*: SettingsModal + fonts*: array[Config.fonts.len, ptr ImFont] resources*: Table[string, string] - ImageData* = tuple[image: seq[byte], width, height: int] + maxLabelWidth*: float32 # For the settings modal -implDefaults(Config, {DefaultFlag.defExported}) + ImageData* = tuple[image: seq[byte], width, height: int] # proc renameHook*(_: typedesc[Setting], fieldName: var string) = # fieldName = diff --git a/src/utils.nim b/src/utils.nim index 5568a92..5df4368 100644 --- a/src/utils.nim +++ b/src/utils.nim @@ -1,4 +1,4 @@ -import std/[typetraits, strutils, tables] +import std/[typetraits, strutils, tables, os] import kdl, kdl/prefs import stb_image/read as stbi import nimgl/[imgui, glfw, opengl] @@ -44,9 +44,9 @@ proc res*(app: App, path: string): string = when defined(release): app.resources[path] else: + assert path.fileExists(), path readFile(path) - proc cmpIgnoreStyle(a, b: openarray[char], ignoreChars = {'_', '-'}): int = let aLen = a.len let bLen = b.len @@ -204,15 +204,15 @@ proc loadTextureFromData*(data: var ImageData, outTexture: var GLuint) = glTexImage2D(GL_TEXTURE_2D, GLint 0, GL_RGBA.GLint, GLsizei data.width, GLsizei data.height, GLint 0, GL_RGBA, GL_UNSIGNED_BYTE, data.image[0].addr) -template settingBefore() = - igText(id); igSameLine(0, 0) - igDummy(igVec2(maxLabelWidth - igCalcTextSize(id).x, 0)) - igSameLine(0, 0) +# template settingBefore() = +# igText(id); igSameLine(0, 0) +# igDummy(igVec2(maxLabelWidth - igCalcTextSize(id).x, 0)) +# igSameLine(0, 0) -proc inputText(id: string, value: var string, flags = 0.ImGuiInputTextFlags) = - let maxbuf = buffer.len + 1 - let buffer = newString(maxbuf, value) +# proc inputText(id: string, value: var string, flags = 0.ImGuiInputTextFlags) = +# let maxbuf = buffer.len + 1 +# let buffer = newString(maxbuf, value) - if igInputText(cstring id, cstring buffer, flags): - value = buffer.cleanString() +# if igInputText(cstring id, cstring buffer, flags): +# value = buffer.cleanString()