diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 71011eb..ae86d06 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -16,13 +16,14 @@ jobs:
- name: Build
run: |
+ nimble install -d -y
nimble buildapp
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: appimage-amd64
- path: ./*-*-*.AppImage
+ path: ./*.AppImage
if-no-files-found: error
windows:
@@ -32,10 +33,12 @@ jobs:
- uses: iffy/install-nim@v4.1.1
- name: Build
- run: nimble buildr
+ run: |
+ nimble install -d -y
+ nimble buildr
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: win-amd64
- path: ./*-*-*.exe
+ path: ./*.exe
if-no-files-found: error
diff --git a/ImExample.nimble b/ImExample.nimble
index 61d81af..6691bc9 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 >= 2.0.1"
requires "nimgl >= 1.3.2"
requires "stb_image >= 2.5"
-requires "imstyle >= 1.0.0"
-requires "openurl >= 2.0.3"
+requires "imstyle >= 3.0.0"
+requires "openurl >= 2.0.4"
requires "tinydialogs >= 1.0.0"
+requires "constructor >= 1.2.0"
import std/[strformat, options]
-import src/types
-import kdl
+import src/configtype
-const configPath {.strdefine.} = "config.kdl"
-const config = parseKdlFile(configPath).decode(Config)
+const config = Config()
version = config.version
namedBin["main"] = config.name
@@ -29,13 +28,12 @@ let arch = getEnv("ARCH", "amd64")
let outPath = getEnv("OUTPATH", toExe &"{config.name}-{version}-{arch}")
let flags = getEnv("FLAGS")
-let args = &"--app:gui --out:{outPath} --cpu:{arch} -d:configPath={configPath} {flags}"
+let args = &"--app:gui --out:{outPath} --cpu:{arch} {flags}"
task buildr, "Build the application for release":
- exec "nimble install -d -y"
- exec &"nim cpp -d:release {args} main.nim"
+ exec &"nimble c -d:release {args} main.nim"
-const desktop = """
+const desktopTemplate = """
[Desktop Entry]
Name=$name
Exec=AppRun
@@ -53,18 +51,17 @@ task buildapp, "Build the AppImage":
let appimagePath = &"{config.name}-{version}-{arch}.AppImage"
# Compile applicaiton executable
- if not existsDir("AppDir"): mkDir("AppDir")
- exec "nimble install -d -y"
- exec &"nim cpp -d:release -d:appimage {args} --out:AppDir/AppRun main.nim"
+ if not dirExists("AppDir"): mkDir("AppDir")
+ exec &"nimble c -d:release -d:appimage {args} --out:AppDir/AppRun main.nim"
# Make desktop file
writeFile(
- &"AppDir/{config.name}.desktop",
- desktop % [
- "name", config.name,
- "categories", config.categories.join(";"),
- "version", config.version,
- "comment", config.comment,
+ &"AppDir/{config.name}.desktop",
+ desktopTemplate % [
+ "name", config.name,
+ "categories", config.categories.join(";"),
+ "version", config.version,
+ "comment", config.comment,
"arch", arch
]
)
@@ -72,9 +69,9 @@ task buildapp, "Build the AppImage":
cpFile(config.iconPath, "AppDir/.DirIcon")
cpFile(config.svgIconPath, &"AppDir/{config.name}.svg")
- if config.appstreamPath.isSome:
+ if config.appstreamPath.len > 0:
mkDir("AppDir/usr/share/metainfo")
- cpFile(config.appstreamPath.get, &"AppDir/usr/share/metainfo/{config.name}.appdata.xml")
+ cpFile(config.appstreamPath, &"AppDir/usr/share/metainfo/{config.name}.appdata.xml")
# Get appimagetool
var appimagetoolPath = "appimagetool"
@@ -83,15 +80,15 @@ task buildapp, "Build the AppImage":
exec(&"{appimagetoolPath} --help")
except OSError:
appimagetoolPath = "./appimagetool-x86_64.AppImage"
- if not existsFile(appimagetoolPath):
+ if not fileExists(appimagetoolPath):
echo &"Downloading {appimagetoolPath}"
- exec &"wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O ", appimagetoolPath
+ exec &"wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O {appimagetoolPath}"
exec &"chmod +x {appimagetoolPath}"
# Actually use appimagetool to build the AppImage
if config.ghRepo.isSome:
echo "Building updateable AppImage"
- exec &"{appimagetoolPath} -u \"gh-releases-zsync|{config.ghRepo.get[0]}|{config.ghRepo.get[0]}|latest|{config.name}-*-{arch}.AppImage.zsync\" AppDir {appimagePath}"
+ exec &"{appimagetoolPath} -u \"gh-releases-zsync|{config.ghRepo.get.user}|{config.ghRepo.get.repo}|latest|{config.name}-*-{arch}.AppImage.zsync\" AppDir {appimagePath}"
else:
echo &"ghRepo not defined. Skipping updateable AppImage"
exec &"{appimagetoolPath} AppDir {appimagePath}"
diff --git a/README.md b/README.md
index 06023c0..6e7eae6 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,20 @@
# ImTemplate
-Template for making a single-windowed (or not) Dear ImGui application in Nim.
+Template for making a single-windowed Dear ImGui application in Nim.
-![Main Window](https://user-images.githubusercontent.com/79225325/170889620-d1b3ce74-c92d-440c-9144-92b068973651.png)
+![image](https://github.com/Patitotective/ImTemplate/assets/79225325/6acb8632-1505-4cf9-a520-80255a13c499)
(Check [ImDemo](https://github.com/Patitotective/ImDemo) for a **full** example)
## Features
- Icon font support.
-- Simple about modal.
-- Preferences system (with preferences modal).
+- About modal.
+- Preferences system.
+- Settings modal.
- AppImage support (Linux).
- Updateable AppImage support (with [gh-releases-zsync](https://github.com/AppImage/AppImageSpec/blob/master/draft.md#github-releases)).
-- Simple data resources support.
+- Simple data resources support (embed files into the binary).
- GitHub workflow for building and uploading the AppImage and `.exe` as assets.
+- Non-blocking (using a [`std/threadpool`](https://nim-lang.org/docs/threadpool.html)) native system dialogs using [`tinydialogs`](https://github.com/Patitotective/tinydialogs).
(To use NimGL in Ubuntu you might need some libraries `sudo apt install libxcursor-dev libxrandr-dev libxinerama-dev libxi-dev libgl-dev`)
@@ -20,25 +22,27 @@ Template for making a single-windowed (or not) Dear ImGui application in Nim.
- `README.md`: Project's description.
- `LICENSE`: Project's license.
- `main.nim`: Application's logic.
-- `resourcesdata.nim`: To bundle data resources (see [Bundling](#bundling)).
-- `nakefile.md`: [Nakefile](https://github.com/fowlmouth/nake) to build the AppImage (see [Building](#building)).
+- `resources.nim`: To bundle data resources (see [Bundling](#bundling)).
- `config.nims`: Nim compile configuration.
-- `config.toml`: Application's configuration (see [Config](#config)).
- `ImExample.nimble`: [Nimble file](https://github.com/nim-lang/nimble#creating-packages).
-- `assets`:
+- `assets`:
- `icon.png`, `icon.svg`: App icons.
- - `style.toml`: Style (using [ImStyle](https://github.com/Patitotective/ImStyle)).
+ - `style.kdl`: Style (using [ImStyle](https://github.com/Patitotective/ImStyle)).
- `Cousine-Regular.ttf`, `Karla-Regular.ttf`, `Roboto-Regular.ttf`, `ProggyVector Regular.ttf`: Multiple fonts so you can choose the one you like the most.
- `forkawesome-webfont.ttf`: ForkAwesome icon font (see https://forkaweso.me/).
- `src`:
+ - `types.nim`: Type definitions used by other modules.
- `icons.nim`: Helper module with [ForkAwesome](https://forkaweso.me) icons unicode points.
- `utils.nim`: Useful procedures, general types or anything used by more than one module.
- - `settingsmodal.nim`: Draw the preferences modal (called in `main.nim`)
+ - `settingsmodal.nim`: Draw the settings modal
## Icon Font
-ImTemplate uses [ForkAwesome](https://forkaweso.me)'s icon font to be able to display icon in labes, to do it you only need to import [`icons.nim`](https://github.com/Patitotective/ImTemplate/blob/main/src/icons.toml) (where the unicode points for each icon are defined), browse https://forkaweso.me/Fork-Awesome/icons, choose the one you want and, for example, if you want to use [`fa-floppy-o`](https://forkaweso.me/Fork-Awesome/icon/floppy-o/), you will write `FA_FloppyO` in a string:
+ImTemplate uses [ForkAwesome](https://forkaweso.me)'s icon font to be able to display icon in labels, to do it you only need to import [`icons.nim`](https://github.com/Patitotective/ImTemplate/blob/main/src/icons.nim) (where the unicode points for each icon are defined), browse https://forkaweso.me/Fork-Awesome/icons, choose the one you want and, for example, if you want to use [`fa-floppy-o`](https://forkaweso.me/Fork-Awesome/icon/floppy-o/), you will write `FA_FloppyO` in a string:
```nim
...
+# main.nim
+import src/icons
+
if igButton("Open Link " & FA_ExternalLink):
openURL("https://forkaweso.me")
```
@@ -47,158 +51,158 @@ if igButton("Open Link " & FA_ExternalLink):
The code is designed to rely on the `App` type (defined in [`utils.nim`](https://github.com/Patitotective/ImTemplate/blob/main/src/utils.nim)), you may want to store anything that your program needs inside it.
```nim
type
- App* = ref object
+ App* = object
win*: GLFWWindow
- font*: ptr ImFont
- prefs*: Prefs
- cache*: TomlValueRef # Settings cache
- config*: TomlValueRef # Prefs table
+ config*: Config
+ prefs*: KdlPrefs[Prefs]
+ fonts*: array[Config.fonts.len, ptr ImFont]
+ resources*: Table[string, string]
+ maxLabelWidth*: float32
+ messageBoxResult*: FlowVar[Button]
# Add your variables here
...
```
- `win`: GLFW window.
-- `font`: Default app font (you may want to add more fonts).
-- `prefs`: App preferences (using [niprefs](https://patitotective.github.io/niprefs/)).
-- `cache`: Preferences modal cache settings (to discard or apply them).
+- `fonts`: An array containing the loaded fonts from `Config.fonts`.
+- `prefs`: See [Prefs](#prefs).
- `config`: Configuration file (loaded from `config.toml`).
+- `resources`: Data resources where the key is the filename and the value is the binary data.
+- `maxLabelWidth`: This is a value that's used to draw the settingsmodal (see https://github.com/Patitotective/ImTemplate/blob/main/src/settingsmodal.nim)
+- `messageBoxResult`: This variable stores the result to a message box dialog opened by [`tinydialogs`](https://github.com/Patitotective/tinydialogs), it uses the `FlowVar` type since it's the result of a spawned thread.
## Config
-The application's configuration will store information about the app that you may want to change after compiled and before deployed (like the name or version).
-It is stored using [niprefs](https://patitotective.github.io/niprefs/) and by default at [`config.toml`](https://github.com/Patitotective/ImTemplate/blob/main/config.toml):
+The configuration stores data like name and version of the application, it is stored in its type definition in `src/configtype.nim` using `constructor/defaults` to define the default values:
```nim
-# App
-name = "ImExample"
-comment = "ImExample is a simple Dear ImGui application example"
-version = "0.4.0"
-website = "https://github.com/Patitotective/ImTemplate"
-authors = ["Patitotective ", "Cristobal ", "Omar Cornut ", "Beef, Yard, Rika", "and the Nim community :]", "Inu147"]
-categories = ["Utility"]
-
-# AppImage
-ghRepo = "Patitotective/ImTemplate"
-
-stylePath = "assets/style.toml"
-iconPath = "assets/icon.png"
-svgIconPath = "assets/icon.svg"
-iconFontPath = "assets/forkawesome-webfont.ttf"
-fontPath = "assets/ProggyVector Regular.ttf" # Other options are Roboto-Regular.ttf, Cousine-Regular.ttf or Karla-Regular.ttf
-fontSize = 16.0
-
-# Window
-minSize = [200, 200] # Width, height
-
-# Settings for the preferences window
-[settings.input]
-type = "input"
-default = "Hello World"
-max = 100
-flags = "None" # See https://nimgl.dev/docs/imgui.html#ImGuiInputTextFlags
-help = "Help message"
-...
+type
+ Config* = object
+ name* = "ImExample"
+ comment* = "ImExample is a simple Dear ImGui application example"
+ version* = "2.0.0"
+ website* = "https://github.com/Patitotective/ImTemplate"
+ authors* = [
+ (name: "Patitotective", url: "https://github.com/Patitotective"),
+ ("Cristobal", "mailto:cristobalriaga@gmail.com"),
+ ("Omar Cornut", "https://github.com/ocornut"),
+ ("Beef, Yard, Rika", ""),
+ ("and the Nim community :]", ""),
+ ("Inu147", ""),
+ ]
+ categories* = "Utility"
+
+ stylePath* = "assets/style.kdl"
+ iconPath* = "assets/icon.png"
+ svgIconPath* = "assets/icon.svg"
+
+ iconFontPath* = "assets/forkawesome-webfont.ttf"
+ 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* = (user: "Patitotective", repo: "ImTemplate").some
+ appstreamPath* = ""
+
+ # Window
+ minSize* = (w: 200i32, h: 200i32) # < 0: don't care
```
-
-### About Modal
-Using the information from the config file, ImTemplate creates a simple about modal.
-
-![About Modal](https://user-images.githubusercontent.com/79225325/170889730-8cba620b-3d6d-4574-8228-5c45930821d1.png)
-
-### Keys Explanation
-- `name`: App name.
-- `comment`: App description.
-- `version`: App version.
+### Fields Explanation
+- `name`: App's name.
+- `comment`: App's description.
+- `version`: App's version.
- `website`: A link where you can find more information about the app.
-- `authors`: A sequence of strings to display in the about modal, a link for the author can be specified inside `<>`, e.i.: `@["Patitotective ", "Cristobal "]`.
+- `authors`: An array containing information about the authors.
- `categories`: Sequence of [registered categories](https://specifications.freedesktop.org/menu-spec/latest/apa.html) (for the AppImage).
-
-(AppImage)
-- `ghRepo`: GitHub repo to fetch releases from (including this key will generate an `AppImage.zsync` file, include it in your releases for [updates](https://docs.appimage.org/packaging-guide/optional/updates.html#using-appimagetool), skip it to disable).
-- `appstreamPath`: Path to the [AppStream metadata](https://docs.appimage.org/packaging-guide/optional/appstream.html) (optional).
-
-(Paths)
-- `stylePath`: App style path (using https://github.com/Patitotective/ImStyle).
-- `iconPath`: Icon path.
+- `stylePath`: App's ImStyle path (using https://github.com/Patitotective/ImStyle).
+- `iconPath`: PNG icon path.
- `svgIconPath`: Scalable icon path
- `iconFontPath`: [ForkAwesome](https://forkaweso.me)'s font path.
-- `fontPath`: Font path.
-- `fontSize`: Font size.
+- `fonts`: An array of `Font` objects containing the font's path, size and range of glyphs for japanese, korean, chinese, etc.
+- `ghRepo`: GitHub repo to fetch releases from (if it's some it will generate an `AppImage.zsync` file, include it in your releases for [AppImage updates](https://docs.appimage.org/packaging-guide/optional/updates.html#using-appimagetool)).
+- `appstreamPath`: Path to the [AppStream metadata](https://docs.appimage.org/packaging-guide/optional/appstream.html).
+- `minSize`: Window's minimum size, use numbers less than zero to disable a limit.
-- `minSize`: Window's minimum size.
-- `settings`: See [`settings`](#settings).
-
-### `settings`
-Define the preferences that the user can modify through the preferences modal.
-
-These preferences will be stored at `getCacheDir(config["name"])` along with the window size and position using [niprefs](https://patitotective.github.io/niprefs/). To acces them you only need to do `app.prefs["name"]`
+### About Modal
+Using the information from the config object, ImTemplate creates a simple about modal.
-![Prefs Modal](https://user-images.githubusercontent.com/79225325/170889748-316c4b7a-47d0-4a65-82b3-d4e50b9252ea.png)
+![image](https://github.com/Patitotective/ImTemplate/assets/79225325/bd018f26-4d8f-4dd4-a7ea-cfece401a3b5)
-Each child key has to have the `type` key, and depending on it the required keys may change so go check [config.toml](https://github.com/Patitotective/ImTemplate/blob/main/config.toml) to see which keys which types do require.
+## Prefs
+The preferences are data can change during runtime and data that you want to store for the future like the position and size of the window, this includes the settings like the language and theme.
+The preferences are saved in a KDL file (using [kdl/prefs](https://patitotective.github.io/kdl-nim/kdl/prefs.html)).
+You just have to provide an object including all the data you want to store as fields:
```nim
-[settings.combo]
-type = "combo"
-default = 2 # Or "c"
-items = ["a", "b", "c"]
-flags = "None" # See https://nimgl.dev/docs/imgui.html#ImGuiComboFlags
+type
+ Prefs* {.defaults: {defExported}.} = object
+ maximized* = false # Was the window maximized when the app was closed?
+ winpos* = (x: -1i32, y: -1i32) # Window position
+ winsize* = (w: 600i32, h: 650i32) # Window size
+ settings* = initSettings()
```
-There are two special keys, `display` and `help`, `display` replaces the name to display and `help` shows a help marker with help information (`help` does not work for `Section`s).
-To access `combo`'s value in your program you should do `app.prefs["combo"]`
-
-#### Setting types
-- `Input`: Input text.
-- `Check`: Checkbox.
-- `Slider`: Integer slider.
-- `FSlider`: Float slider.
-- `Spin`: Integer spin.
-- `FSpin`: Float spin.
-- `Combo`: Combo.
-- `Radio`: Radio button.
-- `Color3`: Color edit RGB.
-- `Color4`: Color edit RGBA.
-- `Section`: See [`Section`](#section)
-
-#### `Section`
-![Setting Section](https://user-images.githubusercontent.com/79225325/170889758-b7845c4a-df3a-4a06-a0c9-e64b0659097e.png)
-
-Section types are useful to group similar settings.
-It fits the settings at `content` inside a [collapsing header](https://nimgl.dev/docs/imgui.html#igCollapsingHeader%2Ccstring%2CImGuiTreeNodeFlags).
+
+### Settings
+The settings are preferences that the user can modify through the settings modal.
+
+![image](https://github.com/Patitotective/ImTemplate/assets/79225325/0b268d5a-e034-4541-be96-954263bab2ae)
+
+You can define all the settings' settings (i.e.: combobox, checkbox, input, etc.) through the `Settings` object:
```nim
-[settings.colors]
-display = "Color pickers"
-type = "section"
-flags = "None" # See https://nimgl.dev/docs/imgui.html#ImGuiTreeNodeFlags
-[settings.colors.content.color]
-display = "RGB color"
-type = "color3" # RGB
-default = "#000000" # Or [0, 0, 0] or rgb(0, 0, 0) or black
-flags = "None" # See https://nimgl.dev/docs/imgui.html#ImGuiColorEditFlags
-[settings.colors.content.alphaColor]
-display = "RGBA color"
-type = "color4" # RGBA
-default = "rgba(17, 209, 194, 0.64)" # Or [0.06666667014360428, 0.8196078538894653, 0.7607843279838562, 0.6392157077789307]
-flags = "None" # See https://nimgl.dev/docs/imgui.html#ImGuiColorEditFlags
+type
+ Os* {.defaults: {}.} = object
+ file* = fileSetting(display = "Text File", filterPatterns = @["*.txt", "*.nim", "*.kdl", "*.json"])
+ files* = filesSetting(display = "Multiple files", singleFilterDescription = "Anything", default = @[".bashrc", ".profile"])
+ 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 = [1f, 0f, 0.2f])
+ rgba* = rgbaSetting(default = [0.4f, 0.7f, 0f, 0.5f], 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", hint = "Type...",
+ help = "Has a hint, 10 characters maximum and only accepts on return",
+ limits = 0..10, flags = @[ImGuiInputTextFlags.EnterReturnsTrue]
+ )
+ check* = checkSetting(display = "Checkbox", default = true)
+ combo* = comboSetting(display = "Combo box", items = Sizes.toSeq, default = None)
+ radio* = radioSetting(display = "Radio button", items = @[Big, Medium, Small], default = Medium)
+ os* = sectionSetting(display = "File dialogs", help = "Single file, multiple files and folder pickers", content = initOs())
+ numbers* = sectionSetting(display = "Spinners and sliders", content = initNumbers())
+ colors* = sectionSetting(display = "Color pickers", content = initColors())
```
-To access `alphaColor` you will need to do `app.prefs["colors"]["alphaColor"]` or `app.prefs{"colors", "alphaColor"}`.
## Building
-To build your app you may want to run `nimble buildApp` task.
+To build your app you may want to run `nimble buildr` task.
+You can set the following environment variables to change the building process:
+- `ARCH`: the architecture used to compile the binary, by default `amd64`.
+- `OUTPATH`: the path of the binary file (or exe file on Windows), by default "name-version-arch"
+- `FLAGS`: any other flags you want to pass to the compiler, optional.
-_Note: Unfortunately on Window most of the times Nim binaries are flagged as virus, see https://github.com/nim-lang/Nim/issues/17820._
+**_Note: Unfortunately on Window most of the times Nim binaries are flagged as virus, see https://github.com/nim-lang/Nim/issues/17820._**
### Bundling
-To bundle your app resources inside the compiled binary, you only need to go to `resourcesdata.nim` file and define their paths in the `resources` array.
-After that `resourcesdata` is imported in `main.nim`. So when you compile it, it statically reads those files and creates a table with `[path, data]`.
-To access them use `path.getData()`.
-[`resourcesdata.nim`](https://github.com/Patitotective/ImTemplate/blob/main/resourcesdata.nim)
+To bundle your app resources inside the compiled binary, you only need to go to [`resources.nim`](https://github.com/Patitotective/ImTemplate/blob/main/resources.nim) file and define their paths in the `resourcesPaths` array.
+After that `resources` is imported in `main.nim`. So when you compile it, it statically reads those files and creates a table with the binary data.
+To access them use `app.resources["path"]`.
+By default this is how `resourcesPaths` looks like:
```nim
...
-const resourcesPaths = [
- configPath,
- config["iconPath"].getString(),
- config["stylePath"].getString(),
- config["fontPath"].getString(),
- config["iconFontPath"].getString()
-]
+const resourcesPaths = @[
+ config.stylePath,
+ config.iconPath,
+ config.iconFontPath,
+] & config.fonts.mapIt(it.path) # Add the paths of each font
...
```
@@ -206,13 +210,13 @@ const resourcesPaths = [
You can publish your application as a [binary package](https://github.com/nim-lang/nimble#binary-packages) with nimble.
### AppImage (Linux)
-To build your app as an AppImage you will need to run `nake build`, it will install the dependencies, compile the app, check for `appimagetool` (and install it if its not found in the `$PATH`), generate the `AppDir` directory and finally build the AppImage.
-If you included the `ghRepo` key in the config file, it will generate also an `AppImage.zsync` file. You should attach this file along with the `AppImage` to your GitHub release.
-If you included the `appstreamPath` key, it will get copied to `AppDir/usr/share/shareinfo/{config["name"]}.appdata.xml` (see https://docs.appimage.org/packaging-guide/optional/appstream.html).
+To build your app as an AppImage you will need to run `nimble buildapp`, it will install the dependencies, compile the app, check for `appimagetool` (and install it if its not found in the `$PATH`), generate the `AppDir` directory and finally build the AppImage.
+If you included `ghRepo` in the config, it will also generate an `AppImage.zsync` file. You should attach this file along with the `AppImage` to your GitHub release.
+If you included `appstreamPath`, it will get copied to `AppDir/usr/share/shareinfo/{config.name}.appdata.xml` (see https://docs.appimage.org/packaging-guide/optional/appstream.html).
### Creating a release
-ImTemplate has a [`build.yml` workflow](https://github.com/Patitotective/ImTemplate/blob/main/.github/workflows/build.yml) that automatically when you publish a release, builds an AppImage and an `.exe` file to then upload them as assets to the release.
-This can take several minutes.
+ImTemplate has a [`build.yml` workflow](https://github.com/Patitotective/ImTemplate/blob/main/.github/workflows/build.yml) that automatically when you publish a release, builds an AppImage and an `.exe` file to then upload them as assets to the release.
+This can take several minutes.
## Generated from ImTemplate
Apps using this template:
diff --git a/config.kdl b/config.kdl
index 324ed63..59a3b19 100644
--- a/config.kdl
+++ b/config.kdl
@@ -21,7 +21,6 @@ fonts iconFontPath="assets/forkawesome-webfont.ttf" {
- "assets/ProggyVector Regular.ttf" 16 // Other options are Roboto-Regular.ttf, Cousine-Regular.ttf or Karla-Regular.ttf
- "assets/NotoSansJP-Regular.otf" 16
}
-
// AppImage
ghRepo "Patitotective" "ImTemplate"
@@ -94,3 +93,4 @@ settings {
}
}
}
+
diff --git a/config.nims b/config.nims
index a376641..a8f4629 100644
--- a/config.nims
+++ b/config.nims
@@ -1,5 +1,7 @@
switch("backend", "cpp")
switch("warning", "HoleEnumConv:off")
+switch("warning", "ImplicitDefaultValue:off")
+switch("threads", "on")
when defined(Windows):
switch("passC", "-static")
diff --git a/main.nim b/main.nim
index 5f6cfb7..aaf1b68 100644
--- a/main.nim
+++ b/main.nim
@@ -1,4 +1,4 @@
-import std/[strutils, os]
+import std/[threadpool, strutils, strformat, os]
import imstyle
import openurl
@@ -11,19 +11,11 @@ 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 =
+proc getConfigDir(app: App): string =
getConfigDir() / app.config.name
-proc drawAboutModal(app: App) =
- var center: ImVec2
- getCenterNonUDT(center.addr, igGetMainViewport())
- igSetNextWindowPos(center, Always, igVec2(0.5f, 0.5f))
-
+proc drawAboutModal(app: App) =
+ igSetNextWindowPos(igGetMainViewport().getCenter(), Always, igVec2(0.5f, 0.5f))
let unusedOpen = true # Passing this parameter creates a close button
if igBeginPopupModal(cstring "About " & app.config.name & "###about", unusedOpen.unsafeAddr, flags = makeFlags(ImGuiWindowFlags.NoResize)):
# Display icon image
@@ -35,12 +27,12 @@ proc drawAboutModal(app: App) =
igImage(cast[ptr ImTextureID](texture), igVec2(64, 64)) # Or igVec2(image.width.float32, image.height.float32)
if igIsItemHovered() and app.config.website.len > 0:
igSetTooltip(cstring app.config.website & " " & FA_ExternalLink)
-
+
if igIsMouseClicked(ImGuiMouseButton.Left):
app.config.website.openURL()
igSameLine()
-
+
igPushTextWrapPos(250)
igTextWrapped(cstring app.config.comment)
igPopTextWrapPos()
@@ -58,7 +50,7 @@ proc drawAboutModal(app: App) =
url.openURL()
if igIsItemHovered() and url.len > 0:
igSetTooltip(cstring url & " " & FA_ExternalLink)
-
+
igEndChild()
igSpacing()
@@ -68,7 +60,7 @@ proc drawAboutModal(app: App) =
igEndPopup()
proc drawMainMenuBar(app: var App) =
- var openAbout, openPrefs = false
+ var openAbout, openPrefs, openBlockdialog = false
if igBeginMainMenuBar():
if igBeginMenu("File"):
@@ -79,7 +71,10 @@ proc drawMainMenuBar(app: var App) =
if igBeginMenu("Edit"):
if igMenuItem("Hello"):
- echo "Hello"
+ # If a messageBox hasn't been called or if a called messageBox has already been closed
+ if app.messageBoxResult.isNil or app.messageBoxResult.isReady():
+ app.messageBoxResult = spawn messageBox(app.config.name, "Hello, earthling. Wanna come with us?", DialogType.YesNo, IconType.Question, Button.Yes)
+ openBlockdialog = true
igEndMenu()
@@ -89,24 +84,27 @@ proc drawMainMenuBar(app: var App) =
igMenuItem(cstring "About " & app.config.name, shortcut = nil, p_selected = openAbout.addr)
- igEndMenu()
+ igEndMenu()
igEndMainMenuBar()
# See https://github.com/ocornut/imgui/issues/331#issuecomment-751372071
if openPrefs:
- app.settingsmodal.cache = app.prefs[settings]
+ initCache(app.prefs[settings])
igOpenPopup("Settings")
if openAbout:
igOpenPopup("###about")
+ if openBlockdialog:
+ igOpenPopup("###blockdialog")
# These modals will only get drawn when igOpenPopup(name) are called, respectly
app.drawAboutModal()
app.drawSettingsmodal()
+ # app.drawBlockDialogModal()
proc drawMain(app: var App) = # Draw the main window
- let viewport = igGetMainViewport()
-
+ let viewport = igGetMainViewport()
+
app.drawMainMenuBar()
# Work area is the entire viewport minus main menu bar, task bars, etc.
igSetNextWindowPos(viewport.workPos)
@@ -116,13 +114,19 @@ proc drawMain(app: var App) = # Draw the main window
igText(FA_Info & " Application average %.3f ms/frame (%.1f FPS)", 1000f / igGetIO().framerate, igGetIO().framerate)
if igButton("Click me"):
- notifyPopup(app.config.name, "Do not do that again", IconType.Warning)
+ spawn notifyPopup(app.config.name, "Do not do that again", IconType.Warning)
app.fonts[1].igPushFont()
igText("Unicode fonts (NotoSansJP-Regular.otf)")
- igText("「僕だけがいない街」が好きだった " & FA_SmileO)
+ igText("日本語の言葉 " & FA_SmileO)
igPopFont()
+ if not app.messageBoxResult.isNil and app.messageBoxResult.isReady:
+ if ^app.messageBoxResult == Button.Yes:
+ igText("Glad you said yes!")
+ else:
+ igText("Prepare yourself for the consequences...")
+
igEnd()
proc render(app: var App) = # Called in the main loop
@@ -148,12 +152,12 @@ proc render(app: var App) = # Called in the main loop
glClearColor(bgColor.x, bgColor.y, bgColor.z, bgColor.w)
glClear(GL_COLOR_BUFFER_BIT)
- igOpenGL3RenderDrawData(igGetDrawData())
+ igOpenGL3RenderDrawData(igGetDrawData())
app.win.makeContextCurrent()
app.win.swapBuffers()
-proc initWindow(app: var App) =
+proc initWindow(app: var App) =
glfwWindowHint(GLFWContextVersionMajor, 3)
glfwWindowHint(GLFWContextVersionMinor, 3)
glfwWindowHint(GLFWOpenglForwardCompat, GLFW_TRUE)
@@ -163,9 +167,10 @@ proc initWindow(app: var App) =
glfwWindowHint(GLFWMaximized, GLFW_TRUE)
app.win = glfwCreateWindow(
- app.prefs[winsize].x,
- app.prefs[winsize].y,
- cstring app.config.name,
+ 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,92 +181,83 @@ 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),
+ monitorX + int32((videoMode.width - width) / 2),
monitorY + int32((videoMode.height - height) / 2)
)
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 =
+proc initApp(): App =
when defined(release):
result.resources = readResources()
- result.config = result.res(configPath).parseKdl().decode(Config)
- result.checkSettings()
+ result.config = Config()
- result.initPrefs()
+ let filename =
+ when defined(release): "prefs"
+ else: "prefs_dev"
-template initFonts(app: var App) =
+ let path = (result.getConfigDir() / filename).changeFileExt("kdl")
+
+ try:
+ result.prefs = initKPrefs(
+ path = path,
+ default = initPrefs()
+ )
+ except KdlError:
+ let m = messageBox(result.config.name, &"Corrupt preferences file {path}.\nYou cannot continue using the app until it is fixed.\nYou may fix it manually or do you want to delete it and reset its content? You cannot undo this action", DialogType.OkCancel, IconType.Error, Button.No)
+ if m == Button.Yes:
+ discard tryRemoveFile(path)
+ result.prefs = initKPrefs(
+ path = path,
+ default = initPrefs()
+ )
+ else:
+ raise
+
+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, font in app.config.fonts:
+ let glyph_ranges =
+ 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()
- for e, (path, size) in app.config.fonts.fonts:
- let glyph_ranges =
- case e
- of 1: io.fonts.getGlyphRangesJapanese()
- else: nil
+ app.fonts[e] = io.fonts.igAddFontFromMemoryTTF(app.res(font.path), font.size, glyph_ranges = glyph_ranges)
- 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)
+ # 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) =
+ sync() # Wait for spawned threads
-proc terminate(app: var App) =
var x, y, width, height: int32
app.win.getWindowPos(x.addr, y.addr)
app.win.getWindowSize(width.addr, height.addr)
-
+
app.prefs[winpos] = (x, y)
app.prefs[winsize] = (width, height)
app.prefs[maximized] = app.win.getWindowAttrib(GLFWMaximized) == GLFW_TRUE
@@ -274,7 +270,7 @@ proc main() =
# Setup Window
doAssert glfwInit()
app.initWindow()
-
+
app.win.makeContextCurrent()
glfwSwapInterval(1) # Enable vsync
@@ -295,18 +291,20 @@ proc main() =
app.initFonts()
# Main loop
+ # discard app.win.setWindowCloseCallback(closeCallback(, app.config.name))
while not app.win.windowShouldClose:
app.render()
# Cleanup
igOpenGL3Shutdown()
igGlfwShutdown()
-
+
igDestroyContext()
-
+
app.terminate()
app.win.destroyWindow()
glfwTerminate()
when isMainModule:
main()
+
diff --git a/resources.nim b/resources.nim
index c4301ce..b46fefa 100644
--- a/resources.nim
+++ b/resources.nim
@@ -1,17 +1,15 @@
import std/[sequtils, tables]
import kdl
-import src/types
+import src/configtype
-const configPath {.strdefine.} = "config.kdl"
-const config = parseKdlFile(configPath).decode(Config)
+const config = Config()
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/configtype.nim b/src/configtype.nim
new file mode 100644
index 0000000..7fb016e
--- /dev/null
+++ b/src/configtype.nim
@@ -0,0 +1,48 @@
+import std/options
+
+type
+
+ 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* = object
+ name* = "ImExample"
+ comment* = "ImExample is a simple Dear ImGui application example"
+ version* = "2.0.0"
+ website* = "https://github.com/Patitotective/ImTemplate"
+ authors* = [
+ (name: "Patitotective", url: "https://github.com/Patitotective"),
+ ("Cristobal", "mailto:cristobalriaga@gmail.com"),
+ ("Omar Cornut", "https://github.com/ocornut"),
+ ("Beef, Yard, Rika", ""),
+ ("and the Nim community :]", ""),
+ ("Inu147", ""),
+ ]
+ categories* = ["Utility"]
+
+ stylePath* = "assets/style.kdl"
+ iconPath* = "assets/icon.png"
+ svgIconPath* = "assets/icon.svg"
+
+ iconFontPath* = "assets/forkawesome-webfont.ttf"
+ 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* = (user: "Patitotective", repo: "ImTemplate").some
+ appstreamPath* = ""
+
+ # Window
+ minSize* = (w: 200i32, h: 200i32) # < 0: don't care
+
diff --git a/src/igutils.nim b/src/igutils.nim
deleted file mode 100644
index 153da75..0000000
--- a/src/igutils.nim
+++ /dev/null
@@ -1,134 +0,0 @@
-import stb_image/read as stbi
-import nimgl/[imgui, glfw, opengl]
-
-import types
-
-proc `+`*(vec1, vec2: ImVec2): ImVec2 =
- ImVec2(x: vec1.x + vec2.x, y: vec1.y + vec2.y)
-
-proc `-`*(vec1, vec2: ImVec2): ImVec2 =
- ImVec2(x: vec1.x - vec2.x, y: vec1.y - vec2.y)
-
-proc `*`*(vec1, vec2: ImVec2): ImVec2 =
- ImVec2(x: vec1.x * vec2.x, y: vec1.y * vec2.y)
-
-proc `/`*(vec1, vec2: ImVec2): ImVec2 =
- ImVec2(x: vec1.x / vec2.x, y: vec1.y / vec2.y)
-
-proc `+`*(vec: ImVec2, val: float32): ImVec2 =
- ImVec2(x: vec.x + val, y: vec.y + val)
-
-proc `-`*(vec: ImVec2, val: float32): ImVec2 =
- ImVec2(x: vec.x - val, y: vec.y - val)
-
-proc `*`*(vec: ImVec2, val: float32): ImVec2 =
- ImVec2(x: vec.x * val, y: vec.y * val)
-
-proc `/`*(vec: ImVec2, val: float32): ImVec2 =
- ImVec2(x: vec.x / val, y: vec.y / val)
-
-proc `+=`*(vec1: var ImVec2, vec2: ImVec2) =
- vec1.x += vec2.x
- vec1.y += vec2.y
-
-proc `-=`*(vec1: var ImVec2, vec2: ImVec2) =
- vec1.x -= vec2.x
- vec1.y -= vec2.y
-
-proc `*=`*(vec1: var ImVec2, vec2: ImVec2) =
- vec1.x *= vec2.x
- vec1.y *= vec2.y
-
-proc `/=`*(vec1: var ImVec2, vec2: ImVec2) =
- vec1.x /= vec2.x
- vec1.y /= vec2.y
-
-proc igVec2*(x, y: float32): ImVec2 = ImVec2(x: x, y: y)
-
-proc igVec4*(x, y, z, w: float32): ImVec4 = ImVec4(x: x, y: y, z: z, w: w)
-
-proc igHSV*(h, s, v: float32, a: float32 = 1f): ImColor =
- result.addr.hSVNonUDT(h, s, v, a)
-
-proc igGetContentRegionAvail*(): ImVec2 =
- igGetContentRegionAvailNonUDT(result.addr)
-
-proc igGetWindowPos*(): ImVec2 =
- igGetWindowPosNonUDT(result.addr)
-
-proc igCalcTextSize*(text: cstring, text_end: cstring = nil, hide_text_after_double_hash: bool = false, wrap_width: float32 = -1.0'f32): ImVec2 =
- igCalcTextSizeNonUDT(result.addr, text, text_end, hide_text_after_double_hash, wrap_width)
-
-proc igCalcFrameSize*(text: string): ImVec2 =
- igCalcTextSize(cstring text) + (igGetStyle().framePadding * 2)
-
-proc igColorConvertU32ToFloat4*(color: uint32): ImVec4 =
- igColorConvertU32ToFloat4NonUDT(result.addr, color)
-
-proc getCenter*(self: ptr ImGuiViewport): ImVec2 =
- getCenterNonUDT(result.addr, self)
-
-proc igCenterCursorX*(width: float32, align: float = 0.5f, avail = igGetContentRegionAvail().x) =
- let off = (avail - width) * align
-
- if off > 0:
- igSetCursorPosX(igGetCursorPosX() + off)
-
-proc igCenterCursorY*(height: float32, align: float = 0.5f, avail = igGetContentRegionAvail().y) =
- let off = (avail - height) * align
-
- if off > 0:
- igSetCursorPosY(igGetCursorPosY() + off)
-
-proc igCenterCursor*(size: ImVec2, alignX: float = 0.5f, alignY: float = 0.5f, avail = igGetContentRegionAvail()) =
- igCenterCursorX(size.x, alignX, avail.x)
- igCenterCursorY(size.y, alignY, avail.y)
-
-proc igHelpMarker*(text: string) =
- igTextDisabled("(?)")
- if igIsItemHovered():
- igBeginTooltip()
- igPushTextWrapPos(igGetFontSize() * 35.0)
- igTextUnformatted(text)
- igPopTextWrapPos()
- igEndTooltip()
-
-proc newImFontConfig*(mergeMode = false): ImFontConfig =
- result.fontDataOwnedByAtlas = true
- result.fontNo = 0
- result.oversampleH = 3
- result.oversampleV = 1
- result.pixelSnapH = true
- result.glyphMaxAdvanceX = float.high
- result.rasterizerMultiply = 1.0
- result.mergeMode = mergeMode
-
-proc igAddFontFromMemoryTTF*(self: ptr ImFontAtlas, data: string, size_pixels: float32, font_cfg: ptr ImFontConfig = nil, glyph_ranges: ptr ImWchar = nil): ptr ImFont {.discardable.} =
- let igFontStr = cast[cstring](igMemAlloc(uint data.len))
- igFontStr[0].unsafeAddr.copyMem(data[0].unsafeAddr, data.len)
- result = self.addFontFromMemoryTTF(igFontStr, int32 data.len, sizePixels, font_cfg, glyph_ranges)
-
-proc initGLFWImage*(data: ImageData): GLFWImage =
- result = GLFWImage(pixels: cast[ptr cuchar](data.image[0].unsafeAddr), width: int32 data.width, height: int32 data.height)
-
-proc readImageFromMemory*(data: string): ImageData =
- var channels: int
- result.image = stbi.loadFromMemory(cast[seq[byte]](data), result.width, result.height, channels, stbi.Default)
-
-proc loadTextureFromData*(data: var ImageData, outTexture: var GLuint) =
- # Create a OpenGL texture identifier
- glGenTextures(1, outTexture.addr)
- glBindTexture(GL_TEXTURE_2D, outTexture)
-
- # Setup filtering parameters for display
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR.GLint)
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR.GLint)
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE.GLint) # This is required on WebGL for non power-of-two textures
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE.GLint) # Same
-
- # Upload pixels into texture
- # if defined(GL_UNPACK_ROW_LENGTH) && !defined(__EMSCRIPTEN__)
- glPixelStorei(GL_UNPACK_ROW_LENGTH, 0)
-
- 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)
-
diff --git a/src/settingsmodal.nim b/src/settingsmodal.nim
index 2ac1d5b..5aef20c 100644
--- a/src/settingsmodal.nim
+++ b/src/settingsmodal.nim
@@ -1,205 +1,200 @@
-import std/[strutils, options, tables, os]
-
+import std/[threadpool, typetraits, strutils, options, tables, macros, os]
+import micros
import kdl/prefs
import tinydialogs
import nimgl/imgui
import utils, icons, types
-proc drawSettings(settings: var object, settingsConfig: OrderedTable[string, Setting], maxLabelWidth: float32) =
- for name, field in settings.fieldPairs:
- var key: string
-
- 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 settingLabel(name: string, setting: Setting[auto]): string =
+ (if setting.display.len == 0: name else: setting.display) & ": "
+
+proc drawSettings(settings: var object, maxLabelWidth: float32): bool =
+ ## Returns wheter or not to open the block dialog (because a file dailog or so was open)
+
+ for name, setting in settings.fieldPairs:
+ let label = settingLabel(name, setting)
let id = cstring "##" & name
- if data.kind != stSection:
- igText(label); igSameLine(0, 0)
- igDummy(igVec2(maxLabelWidth - igCalcTextSize(label).x, 0))
+ if setting.kind != stSection:
+ igText(cstring label); igSameLine(0, 0)
+ if igIsItemHovered():
+ if igIsMouseReleased(ImGuiMouseButton.Right):
+ igOpenPopup(cstring label)
+ elif setting.help.len > 0:
+ igSetToolTip(cstring setting.help)
+
+ igDummy(igVec2(maxLabelWidth - igCalcTextSize(cstring 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()
+ let flags = makeFlags(setting.inputFlags)
+ let buffer = newString(setting.limits.b, setting.inputCache)
+
+ if setting.hint.len > 0:
+ if igInputTextWithHint(id, cstring setting.hint, cstring buffer, uint setting.limits.b, flags) and (let newBuffer = buffer.cleanString(); newBuffer.len >= setting.limits.a):
+ setting.inputCache = newBuffer
+ else:
+ if igInputText(id, cstring buffer, uint setting.limits.b, flags) and (let newBuffer = buffer.cleanString(); newBuffer.len >= setting.limits.a):
+ setting.inputCache = newBuffer
of stCheck:
- assert field is bool
- when field is bool:
- igCheckbox(id, field.addr)
+ igCheckbox(id, setting.checkCache.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)
- )
+ igSliderInt(
+ id,
+ setting.sliderCache.addr,
+ setting.sliderRange.a,
+ setting.sliderRange.b,
+ cstring setting.sliderFormat,
+ makeFlags(setting.sliderFlags)
+ )
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
+ igSliderFloat(
+ id,
+ setting.fsliderCache.addr,
+ setting.fsliderRange.a,
+ setting.fsliderRange.b,
+ cstring setting.fsliderFormat,
+ makeFlags(setting.fsliderFlags)
+ )
+ of stSpin:
+ var temp = setting.spinCache
+ if igInputInt(
+ id,
+ temp.addr,
+ setting.step,
+ setting.stepFast,
+ makeFlags(setting.spinflags)
+ ) and temp in setting.spinRange:
+ setting.spinCache = 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
+ var temp = setting.fspinCache
+ if igInputFloat(
+ id,
+ temp.addr,
+ setting.fstep,
+ setting.fstepFast,
+ cstring setting.fspinFormat,
+ makeFlags(setting.fspinflags)
+ ) and temp in setting.fspinRange:
+ setting.fspinCache = 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()
+ if igBeginCombo(id, cstring $setting.comboCache, makeFlags(setting.comboFlags)):
+ for item in setting.comboItems:
+ if igSelectable(cstring $item, item == setting.comboCache):
+ setting.comboCache = item
+ igEndCombo()
+ of stRadio:
+ for e, item in setting.radioItems:
+ if igRadioButton(cstring $item & "##" & name, item == setting.radioCache):
+ setting.radioCache = item
+
+ if e < setting.radioItems.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])
+ igColorEdit3(id, setting.rgbCache, makeFlags(setting.rgbFlags))
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])
+ igColorEdit4(id, setting.rgbaCache, makeFlags(setting.rgbaFlags))
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()
+ if not setting.fileCache.flowvar.isNil and setting.fileCache.flowvar.isReady and (let val = ^setting.fileCache.flowvar; val.len > 0):
+ setting.fileCache = (val: val, flowvar: nil) # Here we set flowvar to nil because once we acquire it's value it's not neccessary until it's spawned again
+
+ igPushID(id)
+ igInputTextWithHint("##input", "No file selected", cstring setting.fileCache.val, uint setting.fileCache.val.len, flags = ImGuiInputTextFlags.ReadOnly)
+ igSameLine()
+ if igButton("Browse " & FA_FolderOpen):
+ setting.fileCache.flowvar = spawn openFileDialog("Choose File", getCurrentDir() / "\0", setting.fileFilterPatterns, setting.fileSingleFilterDescription)
+ result = true
+ 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:
+ if not setting.filesCache.flowvar.isNil and setting.filesCache.flowvar.isReady and (let val = ^setting.filesCache.flowvar; val.len > 0):
+ setting.filesCache = (val: val, flowvar: nil) # Here we set flowvar to nil because once we acquire it's value it's not neccessary until it's spawned again
+
+ let files = setting.filesCache.val.join(";")
+ igPushID(id)
+ igInputTextWithHint("##input", "No files selected", cstring files, uint files.len, flags = ImGuiInputTextFlags.ReadOnly)
igSameLine()
- igHelpMarker(data.help)
+ if igButton("Browse " & FA_FolderOpen):
+ setting.filesCache.flowvar = spawn openMultipleFilesDialog("Choose Files", getCurrentDir() / "\0", setting.filesFilterPatterns, setting.filesSingleFilterDescription)
+ result = true
+ igPopID()
+ of stFolder:
+ if not setting.folderCache.flowvar.isNil and setting.folderCache.flowvar.isReady and (let val = ^setting.folderCache.flowvar; val.len > 0):
+ setting.folderCache = (val: val, flowvar: nil) # Here we set flowvar to nil because once we acquire it's value it's not neccessary until it's spawned again
-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()) & ": "
+ igPushID(id)
+ igInputTextWithHint("##input", "No folder selected", cstring setting.folderCache.val, uint setting.folderCache.val.len, flags = ImGuiInputTextFlags.ReadOnly)
+ igSameLine()
+ if igButton("Browse " & FA_FolderOpen):
+ setting.folderCache.flowvar = spawn selectFolderDialog("Choose Folder", getCurrentDir() / "\0")
+ result = true
+ igPopID()
+ of stSection:
+ if igCollapsingHeader(cstring label, makeFlags(setting.sectionFlags)):
+ if igIsItemHovered():
+ if igIsMouseReleased(ImGuiMouseButton.Right):
+ igOpenPopup(cstring label)
+
+ igPushID(id); igIndent()
+ when setting.content is object:
+ result = drawSettings(setting.content, maxLabelWidth)
+ igUnindent(); igPopID()
+ else: # When the header is closed
+ if igIsItemHovered():
+ if igIsMouseReleased(ImGuiMouseButton.Right):
+ igOpenPopup(cstring label)
+
+ if igBeginPopup(cstring label):
+ if igSelectable(cstring("Reset " & label[0..^3] #[remove the ": "]# & " to default")):
+ setting.cacheToDefault()
+ igEndPopup()
- if (let width = (
- if data.kind == stSection:
- calcMaxLabelWidth(data.content)
+ if setting.help.len > 0 and setting.kind != stSection:
+ igSameLine()
+ igHelpMarker(setting.help)
+
+proc calcMaxLabelWidth(settings: object): 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:
+ when setting.content is object:
+ calcMaxLabelWidth(setting.content)
+ else: 0f
+ else:
+ igCalcTextSize(cstring 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()
+proc drawSettingsmodal*(app: var App) =
+ if app.maxLabelWidth <= 0:
+ app.maxLabelWidth = app.prefs[settings].calcMaxLabelWidth()
igSetNextWindowPos(igGetMainViewport().getCenter(), Always, igVec2(0.5f, 0.5f))
if igBeginPopupModal("Settings", flags = makeFlags(AlwaysAutoResize, HorizontalScrollbar)):
var close = false
- # app.settingsmodal.cache must be set to app.prefs[settings] once when opening the modal
- drawSettings(app.settingsmodal.cache, app.config.settings, app.settingsmodal.maxLabelWidth)
+ if drawSettings(app.prefs[settings], app.maxLabelWidth):
+ igOpenPopup("###blockdialog")
+
+ app.drawBlockDialogModal()
igSpacing()
if igButton("Save"):
- app.prefs.content.settings = app.settingsmodal.cache
+ app.prefs[settings].save()
igCloseCurrentPopup()
-
+
igSameLine()
if igButton("Cancel"):
- app.settingsmodal.cache = app.prefs[settings]
+ initCache(app.prefs[settings])
igCloseCurrentPopup()
igSameLine()
@@ -222,7 +217,7 @@ proc drawSettingsmodal*(app: var App) =
igCloseCurrentPopup()
igSameLine()
-
+
if igButton("Cancel"):
igCloseCurrentPopup()
diff --git a/src/types.nim b/src/types.nim
index 29a6039..32b2b86 100644
--- a/src/types.nim
+++ b/src/types.nim
@@ -1,9 +1,16 @@
-import std/[strformat, strutils, tables]
+import std/[threadpool, tables]
+import std/macros except eqIdent # since it conflicts with kdl/util.eqIdent
-import kdl, kdl/types
import nimgl/[imgui, glfw]
+import tinydialogs
+import kdl, kdl/[types, utils]
+import constructor/defaults
-type # Config
+import configtype
+
+export configtype
+
+type
SettingType* = enum
stInput # Input text
stCheck # Checkbox
@@ -20,146 +27,334 @@ type # Config
stFiles # Multiple files picker
stFolder # Folder picker
- Setting* = object
+ RGB* = array[3, float32]
+ RGBA* = array[4, float32]
+
+ Empty* = object # https://forum.nim-lang.org/t/10565
+
+ # T is the object for a section and the enum for a radio or combo
+ Setting*[T: object or enum] = object
display*: string
- flags*: seq[string]
help*: string
- format*: Option[string] # Only applies to stSlider, stFSlider, stSpin and stFSpin but https://github.com/nim-lang/RFCs/issues/368
case kind*: SettingType
of stInput:
- maxbuf*: uint
- hint*: Option[string]
- of stCombo, stRadio:
- items*: seq[string]
+ inputVal*, inputDefault*, inputCache*: string
+ inputFlags*: seq[ImGuiInputTextFlags]
+ limits*: Slice[int]
+ hint*: string
+ of stCombo:
+ comboVal*, comboDefault*, comboCache*: T
+ comboFlags*: seq[ImGuiComboFlags]
+ comboItems*: seq[T]
+ of stRadio:
+ radioVal*, radioDefault*, radioCache*: T
+ radioItems*: seq[T]
of stSection:
- content*: OrderedTable[string, Setting]
- of stSlider, stFSlider, stSpin, stFSpin: # Only stSpin and stFSpin actually use step and stepfast but ^^#368^^
- min*, max*: Option[float32]
- step*, stepfast*: float32
- of stFile, stFiles:
- filterPatterns*: seq[string]
- singleFilterDescription*: string
- else: discard
-
- Fonts* = object
- iconFontPath*: string
- fonts*: seq[tuple[path: string, size: float32]]
-
- Config* = object
- name*: string
- comment*: string
- version*: string
- website*: string
- authors*: seq[tuple[name: string, url: string]]
- categories*: seq[string]
- ghRepo*: Option[(string, string)]
- appstreamPath*: Option[string]
- stylePath*: string
- iconPath*: string
- svgIconPath*: string
- fonts*: Fonts
- minSize*: Option[tuple[x, y: int32]]
- settings*: OrderedTable[string, Setting]
+ content*: T
+ sectionFlags*: seq[ImGuiTreeNodeFlags]
+ of stSlider:
+ sliderVal*, sliderDefault*, sliderCache*: int32
+ sliderFormat*: string
+ sliderRange*: Slice[int32]
+ sliderFlags*: seq[ImGuiSliderFlags]
+ of stFSlider:
+ fsliderVal*, fsliderDefault*, fsliderCache*: float32
+ fsliderFormat*: string
+ fsliderRange*: Slice[float32]
+ fsliderFlags*: seq[ImGuiSliderFlags]
+ of stSpin:
+ spinVal*, spinDefault*, spinCache*: int32
+ spinRange*: Slice[int32]
+ spinFlags*: seq[ImGuiInputTextFlags]
+ step*, stepFast*: int32
+ of stFSpin:
+ fspinVal*, fspinDefault*, fspinCache*: float32
+ fspinFormat*: string
+ fspinRange*: Slice[float32]
+ fspinFlags*: seq[ImGuiInputTextFlags]
+ fstep*, fstepFast*: float32
+ of stFile:
+ fileCache*: tuple[val: string, flowvar: FlowVar[string]] # Since flowvar may return an empty string, val keeps the actual value
+ fileVal*, fileDefault*: string
+ fileFilterPatterns*: seq[string]
+ fileSingleFilterDescription*: string
+ of stFiles:
+ filesCache*: tuple[val: seq[string], flowvar: FlowVar[seq[string]]]
+ filesVal*, filesDefault*: seq[string]
+ filesFilterPatterns*: seq[string]
+ filesSingleFilterDescription*: string
+ of stFolder:
+ folderCache*: tuple[val: string, flowvar: FlowVar[string]]
+ folderVal*, folderDefault*: string
+ of stCheck:
+ checkVal*, checkDefault*, checkCache*: bool
+ of stRGB:
+ rgbVal*, rgbDefault*, rgbCache*: array[3, float32]
+ rgbFlags*: seq[ImGuiColorEditFlags]
+ of stRGBA:
+ rgbaVal*, rgbaDefault*, rgbaCache*: RGBA
+ rgbaFlags*: seq[ImGuiColorEditFlags]
+
+# Taken from https://forum.nim-lang.org/t/6781#42294
+proc ifNeqRetFalse(fld,w,v:NimNode):NimNode =
+ quote do:
+ if `w`.`fld` != `v`.`fld`: return false
+proc genIfStmts(recList,i,j:NimNode):NimNode =
+ result = newStmtList()
+ case recList.kind
+ of nnkRecList:
+ for idDef in recList:
+ expectKind(idDef,nnkIdentDefs)
+ result.add idDef[0].ifNeqRetFalse(i,j)
+ of nnkIdentDefs:
+ result.add recList[0].ifNeqRetFalse(i,j)
+ else: error "expected RecList or IdentDefs got" & recList.repr
+
+macro equalsImpl[T:object](a,b:T): untyped =
+ template ifNeqRetFalse(fld:typed):untyped = ifNeqRetFalse(fld,a,b)
+ template genIfStmts(recList:typed):untyped = genIfStmts(recList,a,b)
+
+ let tImpl = a.getTypeImpl
+ result = newStmtList()
+ result.add quote do:
+ result = true
+ let records = tImpl[2]
+ records.expectKind(nnkRecList)
+ for field in records:
+ case field.kind
+ of nnkIdentDefs:
+ result.add field[0].ifNeqRetFalse
+ of nnkRecCase:
+ let discrim = field[0][0]
+ result.add discrim.ifNeqRetFalse
+ var casestmt = newNimNode(nnkCaseStmt)
+ casestmt.add newDotExpr(a,discrim)
+ for ofbranch in field[1..^1]:
+ case ofbranch.kind
+ of nnkOfBranch:
+ let testVal = ofbranch[0]
+ let reclst = ofbranch[1]
+ casestmt.add nnkOfBranch.newTree(testVal,reclst.genIfStmts)
+ of nnkElse:
+ let reclst = ofbranch[0]
+ casestmt.add nnkElse.newTree(reclst.genIfStmts)
+ else: error "Expected OfBranch or Else, got" & ofbranch.repr
+ result.add casestmt
+ else:
+ error "Expected IdentDefs or RecCase, got " & field.repr
+
+proc `==`*[T](a, b: Setting[T]): bool =
+ equalsImpl(a, b)
+
+proc inputSetting(display, help, default, hint = "", limits = 0..100, flags = newSeq[ImGuiInputTextFlags]()): Setting[Empty] =
+ Setting[Empty](display: display, help: help, kind: stInput, inputDefault: default, inputVal: 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, checkVal: default)
+
+proc comboSetting[T: enum](display, help = "", default: T, items: seq[T], flags = newSeq[ImGuiComboFlags]()): Setting[T] =
+ Setting[T](display: display, help: help, kind: stCombo, comboItems: items, comboDefault: default, comboVal: default, comboFlags: flags)
+
+proc radioSetting[T: enum](display, help = "", default: T, items: seq[T]): Setting[T] =
+ Setting[T](display: display, help: help, kind: stRadio, radioItems: items, radioDefault: default, radioVal: 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, sliderVal: 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, fsliderVal: 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, spinVal: 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, fspinVal: 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, fileVal: default, fileFilterPatterns: filterPatterns, fileSingleFilterDescription: singleFilterDescription)
+
+proc filesSetting(display, help = "", default = newSeq[string](), filterPatterns = newSeq[string](), singleFilterDescription = ""): Setting[Empty] =
+ Setting[Empty](display: display, help: help, kind: stFiles, filesDefault: default, filesVal: default, filesFilterPatterns: filterPatterns, filesSingleFilterDescription: singleFilterDescription)
+
+proc folderSetting(display, help, default = ""): Setting[Empty] =
+ Setting[Empty](display: display, help: help, kind: stFolder, folderDefault: default, folderVal: default)
+
+proc rgbSetting(display, help = "", default: RGB, flags = newSeq[ImGuiColorEditFlags]()): Setting[Empty] =
+ Setting[Empty](display: display, help: help, kind: stRGB, rgbDefault: default, rgbVal: default, rgbFlags: flags)
+
+proc rgbaSetting(display, help = "", default: RGBA, flags = newSeq[ImGuiColorEditFlags]()): Setting[Empty] =
+ Setting[Empty](display: display, help: help, kind: stRGBA, rgbaDefault: default, rgbaVal: default, rgbaFlags: flags)
+
+proc toSeq[T: enum](_: typedesc[T]): seq[T] =
+ for i in T:
+ result.add i
+
+type
+ Os* {.defaults: {}.} = object
+ file* = fileSetting(display = "Text File", filterPatterns = @["*.txt", "*.nim", "*.kdl", "*.json"])
+ files* = filesSetting(display = "Multiple files", singleFilterDescription = "Anything", default = @[".bashrc", ".profile"])
+ 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 = [1f, 0f, 0.2f])
+ rgba* = rgbaSetting(default = [0.4f, 0.7f, 0f, 0.5f], 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", hint = "Type...",
+ help = "Has a hint, 10 characters maximum and only accepts on return",
+ limits = 0..10, flags = @[ImGuiInputTextFlags.EnterReturnsTrue]
+ )
+ check* = checkSetting(display = "Checkbox", default = true)
+ combo* = comboSetting(display = "Combo box", items = Sizes.toSeq, default = None)
+ radio* = radioSetting(display = "Radio button", items = @[Big, Medium, Small], default = Medium)
+ os* = sectionSetting(display = "File dialogs", help = "Single file, multiple files and folder pickers", content = initOs())
+ numbers* = sectionSetting(display = "Spinners and sliders", content = initNumbers())
+ colors* = sectionSetting(display = "Color pickers", content = initColors())
+
+proc decodeSettingsObj*(a: KdlNode, v: var object) =
+ # echo "decoding settings ", a
+ for fieldName, field in v.fieldPairs:
+ for child in a.children:
+ if child.name.eqIdent fieldName:
+ case field.kind
+ of stInput:
+ field.inputVal = decodeKdl(child, typeof(field.inputVal))
+ of stCombo:
+ when field.comboVal is enum:
+ field.comboVal = decodeKdl(child, typeof(field.comboVal))
+ else:
+ raise newException(ValueError, $fieldName & " must be an enum, got " & $typeof(field.comboVal))
+ of stCheck:
+ field.checkVal = decodeKdl(child, typeof(field.checkVal))
+ of stSlider:
+ field.sliderVal = decodeKdl(child, typeof(field.sliderVal))
+ of stFSlider:
+ field.fsliderVal = decodeKdl(child, typeof(field.fsliderVal))
+ of stSpin:
+ field.spinVal = decodeKdl(child, typeof(field.spinVal))
+ of stFSpin:
+ field.fspinVal = decodeKdl(child, typeof(field.fspinVal))
+ of stRadio:
+ when field.radioVal is enum:
+ field.radioVal = decodeKdl(child, typeof(field.radioVal))
+ else:
+ raise newException(ValueError, $fieldName & " must be an enum, got " & $typeof(field.radioVal))
+ of stSection:
+ when field.content is object:
+ decodeSettingsObj(child, field.content)
+ else:
+ raise newException(ValueError, $fieldName & " must be an object, got " & $typeof(field.content))
+ of stRGB:
+ field.rgbVal = decodeKdl(child, typeof(field.rgbVal))
+ of stRGBA:
+ field.rgbaVal = decodeKdl(child, typeof(field.rgbaVal))
+ of stFile:
+ field.fileVal = decodeKdl(child, typeof(field.fileVal))
+ of stFiles:
+ field.filesVal = decodeKdl(child, typeof(field.filesVal))
+ of stFolder:
+ field.folderVal = decodeKdl(child, typeof(field.folderVal))
+
+proc decodeKdl*(a: KdlNode, v: var Settings) =
+ v = initSettings()
+ decodeSettingsObj(a, v)
+
+proc encodeKdl*[T](a: FlowVar[T], v: var KdlVal) =
+ if a.isNil or not a.isReady:
+ v = initKNull()
+ else:
+ v = encodeKdlVal(^a)
+
+proc encodeKdl*(a: Empty, v: var KdlVal) =
+ v = initKNull()
+
+proc encodeKdl*(a: seq[string], b: var KdlNode, name: string) =
+ b = initKNode(name)
+ for i in a:
+ b.args.add initKString(i)
+
+proc encodeKdl*[T: Ordinal](a: array[T, float32], b: var KdlNode, name: string) =
+ b = initKNode(name)
+ for i in a:
+ b.args.add initKFloat(i)
+
+proc encodeSettingsObj(a: object): KdlDoc =
+ for fieldName, field in a.fieldPairs:
+ let node =
+ case field.kind
+ of stInput:
+ encodeKdlNode(field.inputVal, $fieldName)
+ of stCombo:
+ when field.comboVal is enum:
+ encodeKdlNode(field.comboVal, $fieldName)
+ else:
+ raise newException(ValueError, $fieldName & " must be an enum, got " & $typeof(field.comboVal))
+ of stCheck:
+ encodeKdlNode(field.checkVal, $fieldName)
+ of stSlider:
+ encodeKdlNode(field.sliderVal, $fieldName)
+ of stFSlider:
+ encodeKdlNode(field.fsliderVal, $fieldName)
+ of stSpin:
+ encodeKdlNode(field.spinVal, $fieldName)
+ of stFSpin:
+ encodeKdlNode(field.fspinVal, $fieldName)
+ of stRadio:
+ when field.comboVal is enum:
+ encodeKdlNode(field.radioVal, $fieldName)
+ else:
+ raise newException(ValueError, $fieldName & " must be an enum, got " & $typeof(field.radioVal))
+ of stSection:
+ when field.content is object:
+ initKNode($fieldName, children = encodeSettingsObj(field.content))
+ else:
+ raise newException(ValueError, $fieldName & " must be an object, got " & $typeof(field.content))
+ of stRGB:
+ encodeKdlNode(field.rgbVal, $fieldName)
+ of stRGBA:
+ encodeKdlNode(field.rgbaVal, $fieldName)
+ of stFile:
+ encodeKdlNode(field.fileVal, $fieldName)
+ of stFiles:
+ encodeKdlNode(field.filesVal, $fieldName)
+ of stFolder:
+ encodeKdlNode(field.folderVal, $fieldName)
+
+ result.add node
+
+proc encodeKdl*(a: Settings, v: var KdlNode, name: string) =
+ v = initKNode(name, children = encodeSettingsObj(a))
type
- Numbers* = object
- slider*, spin*: int32
- floatSlider*, floatSpin*: float32
-
- Colors* = object
- rgb*: tuple[r, g, b: float32]
- rgba*: tuple[r, g, b, a: float32]
-
- Abc* = enum
- A = "a", B = "b", C = "c"
-
- Os* = object
- file*, folder*: string
- files*: seq[string]
-
- Settings* = object
- input*, hintInput*: string
- checkbox*: bool
- combo*, radio*: Abc
- os*: Os
- numbers*: Numbers
- colors*: Colors
-
- Prefs* = object
- maximized*: bool
- winpos*: tuple[x, y: int32]
- winsize*: tuple[x, y: int32]
- settings*: Settings
-
- SettingsModal* = object
- cache*: Settings
- 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]
- fonts*: array[2, ptr ImFont]
- settingsmodal*: SettingsModal
+ prefs*: KdlPrefs[Prefs] # These are the values that will be saved in the prefs file
+ fonts*: array[Config.fonts.len, ptr ImFont]
resources*: Table[string, string]
+ maxLabelWidth*: float32 # For the settings modal
+ messageBoxResult*: FlowVar[Button]
+
ImageData* = tuple[image: seq[byte], width, height: int]
-proc renameHook*(_: typedesc[Setting], fieldName: var string) =
- fieldName =
- case fieldName
- of "type":
- "kind"
- else:
- fieldName
-
-proc enumHook*(a: string, v: var SettingType) =
- try:
- v = parseEnum[SettingType]("st" & a)
- except ValueError:
- raise newException(ValueError, &"invalid enum value {a} for {$typeof(v)}")
-
-proc decodeHook*(a: KdlNode, v: var Fonts) =
- if "iconFontPath" in a.props:
- v.iconFontPath = a["iconFontPath"].getString()
-
- for child in a.children:
- assert child.args.len == 2
- v.fonts.add (child.args[0].getString(), child.args[1].get(float32))
-
-proc decodeHook*(a: KdlNode, v: var (ImVec2 or tuple[x, y: int32])) =
- assert a.args.len == 2
- when v is ImVec2:
- v.x = a.args[0].get(float32)
- v.y = a.args[1].get(float32)
- else:
- v.x = a.args[0].get(int32)
- v.y = a.args[1].get(int32)
-
-proc decodeHook*(a: KdlNode, v: var tuple[name, url: string]) =
- assert a.args.len in 1..2
- v.name = a.args[0].getString()
- if a.args.len > 1:
- v.url = a.args[1].getString()
-
-proc decodeHook*(a: KdlNode, v: var tuple[r, g, b: float32]) =
- assert a.args.len == 3
- v.r = a.args[0].get(float32)
- v.g = a.args[1].get(float32)
- v.b = a.args[2].get(float32)
-
-proc decodeHook*(a: KdlNode, v: var tuple[r, g, b, a: float32]) =
- assert a.args.len == 4
- v.r = a.args[0].get(float32)
- v.g = a.args[1].get(float32)
- v.b = a.args[2].get(float32)
- v.a = a.args[3].get(float32)
-
-proc encodeHook*(a: tuple[r, g, b: float32], v: var KdlNode, name: string) =
- v = initKNode(name, args = toKdlArgs(a.r, a.g, a.b))
-
-proc encodeHook*(a: tuple[r, g, b, a: float32], v: var KdlNode, name: string) =
- v = initKNode(name, args = toKdlArgs(a.r, a.g, a.b, a.a))
-
-proc encodeHook*(a: ImVec2 or tuple[x, y: int32], v: var KdlNode, name: string) =
- v = initKNode(name, args = toKdlArgs(a.x, a.y))
diff --git a/src/utils.nim b/src/utils.nim
index e35e5e9..29d1bc1 100644
--- a/src/utils.nim
+++ b/src/utils.nim
@@ -1,9 +1,10 @@
-import std/[typetraits, strutils, tables]
+import std/[typetraits, threadpool, strutils, tables, macros, os]
import kdl, kdl/prefs
+import stb_image/read as stbi
+import nimgl/[imgui, glfw, opengl]
+import tinydialogs
-import types, igutils
-
-export igutils
+import types
proc makeFlags*[T: enum](flags: varargs[T]): T =
## Mix multiple flags of a specific enum
@@ -13,40 +14,41 @@ proc makeFlags*[T: enum](flags: varargs[T]): T =
result = T res
-proc parseMakeFlags*[T: enum](flags: seq[string]): T =
+proc parseMakeFlags*[T: enum](flags: seq[string]): T =
var res = 0
for x in flags:
res = res or int parseEnum[T](x)
result = T res
-proc pushString*(str: var string, val: string) =
+proc pushString*(str: var string, val: string) =
if val.len < str.len:
str[0..val.len] = val & '\0'
else:
str[0..str.high] = val[0..str.high]
-proc newString*(length: int, default: string): string =
+proc newString*(length: Natural, default: string): string =
result = newString(length)
result.pushString(default)
-proc cleanString*(str: string): string =
- if '\0' in str:
- str[0.. 0:
+ igSetCursorPosX(igGetCursorPosX() + off)
+
+proc igCenterCursorY*(height: float32, align: float = 0.5f, avail = igGetContentRegionAvail().y) =
+ let off = (avail - height) * align
+
+ if off > 0:
+ igSetCursorPosY(igGetCursorPosY() + off)
+
+proc igCenterCursor*(size: ImVec2, alignX: float = 0.5f, alignY: float = 0.5f, avail = igGetContentRegionAvail()) =
+ igCenterCursorX(size.x, alignX, avail.x)
+ igCenterCursorY(size.y, alignY, avail.y)
+
+proc igHelpMarker*(text: string) =
+ igTextDisabled("(?)")
+ if igIsItemHovered():
+ igBeginTooltip()
+ igPushTextWrapPos(igGetFontSize() * 35.0)
+ igTextUnformatted(text)
+ igPopTextWrapPos()
+ igEndTooltip()
+
+proc newImFontConfig*(mergeMode = false): ImFontConfig =
+ result.fontDataOwnedByAtlas = true
+ result.fontNo = 0
+ result.oversampleH = 3
+ result.oversampleV = 1
+ result.pixelSnapH = true
+ result.glyphMaxAdvanceX = float.high
+ result.rasterizerMultiply = 1.0
+ result.mergeMode = mergeMode
+
+proc igAddFontFromMemoryTTF*(self: ptr ImFontAtlas, data: string, size_pixels: float32, font_cfg: ptr ImFontConfig = nil, glyph_ranges: ptr ImWchar = nil): ptr ImFont {.discardable.} =
+ let igFontStr = cast[cstring](igMemAlloc(uint data.len))
+ igFontStr[0].unsafeAddr.copyMem(data[0].unsafeAddr, data.len)
+ result = self.addFontFromMemoryTTF(igFontStr, int32 data.len, sizePixels, font_cfg, glyph_ranges)
+
+proc initGLFWImage*(data: ImageData): GLFWImage =
+ result = GLFWImage(pixels: cast[ptr cuchar](data.image[0].unsafeAddr), width: int32 data.width, height: int32 data.height)
+
+proc readImageFromMemory*(data: string): ImageData =
+ var channels: int
+ result.image = stbi.loadFromMemory(cast[seq[byte]](data), result.width, result.height, channels, stbi.Default)
+
+proc loadTextureFromData*(data: var ImageData, outTexture: var GLuint) =
+ # Create a OpenGL texture identifier
+ glGenTextures(1, outTexture.addr)
+ glBindTexture(GL_TEXTURE_2D, outTexture)
+
+ # Setup filtering parameters for display
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR.GLint)
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR.GLint)
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE.GLint) # This is required on WebGL for non power-of-two textures
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE.GLint) # Same
+
+ # Upload pixels into texture
+ # if defined(GL_UNPACK_ROW_LENGTH) && !defined(__EMSCRIPTEN__)
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, 0)
+
+ 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)
+
+macro checkFlowVarsReady*(app: App, fields: varargs[untyped]): bool =
+ # This macro just converts app.checkFlowVarsReady(field1, field2) to
+ # (app.field1.isNil or app.field1.isReady) and (app.field2.isNil or app.field2.isReady)
+ for field in fields:
+ let cond = quote do:
+ (`app`.`field`.isNil or `app`.`field`.isReady)
+
+ if result.kind == nnkEmpty:
+ result = cond
+ else:
+ result = infix(result, "and", cond)
+
+proc checkSettingsFlowVarsReadyImpl(obj: object): bool =
+ # This macro just converts app.checkFlowVarsReady(field1, field2) to
+ # (app.field1.isNil or app.field1.isReady) and (app.field2.isNil or app.field2.isReady)
+ result = true
+ for fieldName, field in obj.fieldPairs:
+ case field.kind
+ of stFile:
+ if not field.fileCache.flowvar.isNil and not field.fileCache.flowvar.isReady:
+ return false
+ of stFiles:
+ if not field.filesCache.flowvar.isNil and not field.filesCache.flowvar.isReady:
+ return false
+ of stFolder:
+ if not field.folderCache.flowvar.isNil and not field.folderCache.flowvar.isReady:
+ return false
+ of stSection:
+ when field.content is object:
+ if not checkSettingsFlowVarsReadyImpl(field.content):
+ return false
+ else:
+ raise newException(ValueError, $fieldName & " must be an object, got " & $typeof(field.content))
+ else: discard
+
+proc checkFlowVarsReady*(s: Settings): bool =
+ # Converts app.checkFlowVarsReady(field1, field2) to
+ # (app.field1.isNil or app.field1.isReady) and (app.field2.isNil or app.field2.isReady)
+ checkSettingsFlowVarsReadyImpl(s)
+
+proc initCacheSettingsObj(a: var object)
+proc saveSettingsObj(a: var object)
+
+proc valToCache*(s: var Setting) =
+ case s.kind
+ of stInput:
+ s.inputCache = s.inputVal
+ of stCombo:
+ s.comboCache = s.comboVal
+ of stCheck:
+ s.checkCache = s.checkVal
+ of stSlider:
+ s.sliderCache = s.sliderVal
+ of stFSlider:
+ s.fsliderCache = s.fsliderVal
+ of stSpin:
+ s.spinCache = s.spinVal
+ of stFSpin:
+ s.fspinCache = s.fspinVal
+ of stRadio:
+ s.radioCache = s.radioVal
+ of stSection:
+ when s.content is object:
+ initCacheSettingsObj(s.content)
+ else:
+ raise newException(ValueError, $s & " must be an object, got " & $typeof(s.content))
+ of stRGB:
+ s.rgbCache = s.rgbVal
+ of stRGBA:
+ s.rgbaCache = s.rgbaVal
+ of stFile:
+ s.fileCache.val = s.fileVal
+ of stFiles:
+ s.filesCache.val = s.filesVal
+ of stFolder:
+ s.folderCache.val = s.folderVal
+
+proc cacheToVal*(s: var Setting) =
+ case s.kind
+ of stInput:
+ s.inputVal = s.inputCache
+ of stCombo:
+ s.comboVal = s.comboCache
+ of stCheck:
+ s.checkVal = s.checkCache
+ of stSlider:
+ s.sliderVal = s.sliderCache
+ of stFSlider:
+ s.fsliderVal = s.fsliderCache
+ of stSpin:
+ s.spinVal = s.spinCache
+ of stFSpin:
+ s.fspinVal = s.fspinCache
+ of stRadio:
+ s.radioVal = s.radioCache
+ of stSection:
+ when s.content is object:
+ saveSettingsObj(s.content)
+ else:
+ raise newException(ValueError, $s & " must be an object, got " & $typeof(s.content))
+ of stRGB:
+ s.rgbVal = s.rgbCache
+ of stRGBA:
+ s.rgbaVal = s.rgbaCache
+ of stFile:
+ s.fileVal = s.fileCache.val
+ of stFiles:
+ s.filesVal = s.filesCache.val
+ of stFolder:
+ s.folderVal = s.folderCache.val
+
+proc cacheToDefault*(s: var Setting) =
+ case s.kind
+ of stInput:
+ s.inputCache = s.inputDefault
+ of stCombo:
+ s.comboCache = s.comboDefault
+ of stCheck:
+ s.checkCache = s.checkDefault
+ of stSlider:
+ s.sliderCache = s.sliderDefault
+ of stFSlider:
+ s.fsliderCache = s.fsliderDefault
+ of stSpin:
+ s.spinCache = s.spinDefault
+ of stFSpin:
+ s.fspinCache = s.fspinDefault
+ of stRadio:
+ s.radioCache = s.radioDefault
+ of stSection:
+ when s.content is object:
+ initCacheSettingsObj(s.content)
+ else:
+ raise newException(ValueError, $s & " must be an object, got " & $typeof(s.content))
+ of stRGB:
+ s.rgbCache = s.rgbDefault
+ of stRGBA:
+ s.rgbaCache = s.rgbaDefault
+ of stFile:
+ s.fileCache.val = s.fileDefault
+ of stFiles:
+ s.filesCache.val = s.filesDefault
+ of stFolder:
+ s.folderCache.val = s.folderDefault
+
+proc saveSettingsObj(a: var object) =
+ for field in a.fields:
+ field.cacheToVal()
+
+proc initCacheSettingsObj(a: var object) =
+ for field in a.fields:
+ field.valToCache()
+
+proc initCache*(a: var Settings) =
+ ## Sets all a's cache values to the current values (`inputCache = inputVal`)
+ initCacheSettingsObj(a)
+
+proc save*(a: var Settings) =
+ ## Sets all a's current values to the cache values (`inputVal = inputCache`)
+ saveSettingsObj(a)
+
+proc areThreadsFinished*(app: App): bool =
+ app.checkFlowVarsReady(messageBoxResult) and app.prefs[settings].checkFlowVarsReady()
+
+proc drawBlockDialogModal*(app: App) =
+ ## This modal is meant to block the app until all the FlowVar(s) are nil or ready
+ var center: ImVec2
+ getCenterNonUDT(center.addr, igGetMainViewport())
+ igSetNextWindowPos(center, Always, igVec2(0.5f, 0.5f))
+
+ if igBeginPopupModal(cstring "External Dialog###blockdialog", flags = makeFlags(ImGuiWindowFlags.NoResize)):
+ igText("An external dialog is open, \nclose it to continue using the app.")
+
+ # If all spawned threads are finished we can close this popup
+ if app.areThreadsFinished():
+ igCloseCurrentPopup()
+ else: # Do not allow the window to be closed unless the threads are finished
+ if app.win.windowShouldClose():
+ app.win.setWindowShouldClose(false)
+ spawn notifyPopup(app.config.name, "Close the external dialogs before closing the app", IconType.Error)
+
+ igEndPopup()
+
+