#include "canvas.h"
#include "graphchart.h"
#include "msc.h"
#include "blockchart.h"
#include "gui.h"
#include "text_editor.h"
#include "ImGuiFileDialog.h"
#include "utf8utils.h"
#include "error.h"
#include <iostream>
#include <fstream>
#include <string.h>
#include <atomic>
#include <thread>
#include <concepts>

TextureCemetery DeadTextures; ///<Must be before global object Config so that ~EditorChartData() can write to it even from ~ConfigData()

std::map<std::string, std::string> FontLicenses();

std::map<std::string, std::string> RegisterLibrariesGUI() {
    auto ret = FontLicenses();
    ret["Dear ImGui " IMGUI_VERSION];
    ret["ImGui File Dialog " IMGUIFILEDIALOG_VERSION];
    return ret;
}

#define MSC_GEN_INI_FILE_NAME ".msc-generator-ini"

using namespace std::literals;

//normalizes 'a' to fall between 'b' and 'c'.
template <std::totally_ordered T>
inline T between(const T &a, const T &b, const T &c) noexcept { _ASSERT(b<=c); return a<b ? b : c<a ? c : a; }
inline ImVec2 between(ImVec2 a, ImVec2 b, ImVec2 c) noexcept { return {between(a.x, b.x, c.x), between(a.y, b.y, c.y)}; }
inline ImVec2 min(ImVec2 a, ImVec2 b) noexcept { return {std::min(a.x, b.x), std::min(a.y, b.y)}; }
inline ImVec2 max(ImVec2 a, ImVec2 b) noexcept { return {std::max(a.x, b.x), std::max(a.y, b.y)}; }
inline ImVec2 operator+(const ImVec2 &a, const ImVec2 &b) noexcept { return {a.x+b.x, a.y+b.y}; }
inline ImVec2 operator-(const ImVec2 &a, const ImVec2 &b) noexcept { return {a.x-b.x, a.y-b.y}; }
inline ImVec2 operator*(float a, const ImVec2 &b) noexcept { return {a*b.x, a*b.y}; }
inline ImVec2 operator*(const ImVec2 &b, float a) noexcept { return {a*b.x, a*b.y}; }
inline ImVec2 operator/(const ImVec2 &b, float a) noexcept { return {b.x/a, b.y/a}; }
inline bool operator !(const ImVec2 &b) noexcept { return b.x==0.0f && b.y==0.0f; }


std::array<ImFont *, 8> MscGenFonts = {};
float MscGenEditorFontSize;
float MscGenWindowFontSize;

float bookmarkPaneWith = 150.0f; //Adjusted by DPI
float filterComboWith = 300.0f;  //Adjusted by DPI

constexpr bool example_pedantic = false;
constexpr bool example_error_squiggles = true;

ImFont *GetFont(bool variablespaced, bool bold, bool italics) noexcept {
    const unsigned index = (variablespaced ? 4 : 0) + (bold ? 2 : 0) + (italics ? 1 : 0);
    if (MscGenFonts[index]) return MscGenFonts[index];
    if (italics) return GetFont(variablespaced, bold, false);
    if (bold) return GetFont(variablespaced, false, italics);
    if (variablespaced) return GetFont(false, bold, italics);
    _ASSERT(0); //we must have at least the monospaced, non bold, non italics font set
    return ImGui::GetIO().FontDefault;
}

using my_clock = std::chrono::steady_clock;
static auto last_frame = my_clock::now();
inline float sec_since_last_frame() noexcept { return  std::chrono::nanoseconds(my_clock::now()-last_frame).count() / 1e9f; }

//calculates what is the new value of a moving animation going to 'target'
//Speed=1 is when take the main window size is crossed in 3 seconds.
inline float anim_move_value(float orig, float target, float speed = 1.f) {
    const float pixel_per_sec = std::midpoint(ImGui::GetMainViewport()->Size.x, ImGui::GetMainViewport()->Size.y) * speed / 3;
    const float amount = std::min(float(fabs(target-orig)), pixel_per_sec*sec_since_last_frame());
    return orig < target ? orig + amount : orig - amount;
}

//calculates what is the new value of a [0..1] animation going to 'target'
//Speed=1 is when the 0->1 or 1->0 transition takes 1 sec.
inline float anim_time_value(float orig, float target, float speed = 1.f) {
    const float per_sec = speed;
    const float amount = std::min(float(fabs(target-orig)), per_sec*sec_since_last_frame());
    return orig < target ? orig + amount : orig - amount;
}

std::string_view FileNameFromPath(std::string_view full_name) noexcept {
    const auto last_slash_pos = full_name.rfind(directory_separator);
    if (last_slash_pos == full_name.npos) return full_name;
    return full_name.substr(last_slash_pos+1);
}

//removes filename from end of path & all terminating dir separators
//empty when no path comp
//- a.txt->""
//- x/a.txt->"x"
//- x/y/z/a.txt->"x/y/z"
//- x//y/z////a.txt->"x//y/z"
//- x/y/z/->"x/y/z"
std::string_view DirNameFromPath(std::string_view full_name) noexcept {
    const auto last_slash_pos = full_name.rfind(directory_separator);
    if (last_slash_pos == full_name.npos) return {};
    std::string_view ret = full_name.substr(0, last_slash_pos);
    while (ret.size() && ret.back()==directory_separator)
        ret.remove_suffix(1);
    return ret;
}

/** Contains the data used during compilation
 * This should be a completely thread-safe structure*/
class CompileData {
    using clock = std::chrono::steady_clock;
    enum class State { Idle, Compiling, Ready, Stopped };
    std::mutex my_mutex;
    std::atomic<State> state = State::Idle;
    std::atomic_bool stop = false;
    std::atomic_bool destroying = false; //when set, we will no more start a thread.
    std::thread compile_thread;
    clock::time_point compiling_started_ended = clock::now();
    float progress;
    struct Result {
        std::unique_ptr<Chart> pChart;
        int main_file_no; //The number of the currently edited file in pChart->Errors.Files
        TextEditor::UndoBufferIndex compiled_at;
        clock::duration compile_time;
    };
    Result result;
    static bool progressbar(const ProgressBase *progress, void *p,
                            ProgressBase::EPreferredAbortMethod a) {
        CompileData *me = (CompileData *)p;
        me->progress = (float)progress->GetPercentage();
        if (!me->stop.load(std::memory_order_acquire)) return false;
        switch (a) {
        default:
        case ProgressBase::NONE: return false;
        case ProgressBase::EXCEPTION: throw AbortCompilingException();
        case ProgressBase::RETVAL: return true;
        }
    }

public:
    //Stops ongoing compilation, blocks until it finishes and starts a new one
    void Start(std::unique_ptr<Chart> &&chart, std::string &&text, const std::string &fn, TextEditor::UndoBufferIndex editorbuffer);
    //Signals the stop of a compilation. State will move to Stopped, when compile thread detected the signal
    void Stop() noexcept { stop.store(true, std::memory_order::release); }
    //true when compiling, false when ready, stopped or not started
    bool is_compiling() const noexcept { return state.load(std::memory_order_acquire)==State::Compiling; }
    //true when compiling the text referenced by 'at' - or just finished compiling it.
    //in the race when the state changes between evaluating the && condition, we are still OK
    bool is_compiling_at(TextEditor::UndoBufferIndex at) const noexcept { return state.load(std::memory_order_acquire) == State::Compiling && result.compiled_at == at; }
    //true when stopped (will be cleared by next start)
    bool is_stopped() const noexcept { return state.load(std::memory_order_acquire)==State::Stopped; }
    //true when finished or stopped compiling and result is taken.
    bool is_idle() const noexcept { return state.load(std::memory_order_acquire)==State::Idle; }
    //returns time elapsed since compilation started, stopped or ended
    std::chrono::milliseconds time_elapsed() const noexcept { return std::chrono::duration_cast<std::chrono::milliseconds>(clock::now()-compiling_started_ended); }
    //return the progress during compilation, -1 else
    float get_progress() const noexcept {
        std::mutex my_mutex;
        return is_compiling() ? progress : -1.f;
    }
    //Call this before the destructor. Wait until it returns true. Can be called in every frame
    bool Destroy() {
        destroying.store(true, std::memory_order_release);
        Stop();
        GetResult(false);
        const State s = state.load(std::memory_order_acquire);
        return s!=State::Compiling;
    }
    //Atomically returns result if ready and moves to idle
    //If compilation is ongoing, we wait fi 'block' is set, else return empty
    //If there is no result (Stopped, Idle), we set Idle and return empty.
    std::optional<Result> GetResult(bool block) noexcept;
};

/** Contains all the data for hint handling */
struct HintData {
    bool active = false;     ///<True if the hint window needs to be activated
    bool is_user_requested;  ///<True if the current hints session was invoked via Ctrl+Space
    bool search_till_cursor; ///<True if we filter the hints only by the text before the cursor.
    bool replace_till_cursor;///<True if on a hint selected by the user we only replace the part before the cursor.
    bool restart_hint;       ///<True for one frame after substitution if we need to keep doing it.
    std::unique_ptr<Csh> csh;///<Coloring moves the resulting csh here.
    int first_auto_match;    ///<The index of the first item with 'can_autoselect' true, if we do not allow_anything. -1 if none
    struct ItemData {
        TextureID graphics; ///<The hint graphics, empty if none
        bool bold, italics, underline;       ///<Text formatting for the hint
        ImU32 color;                         ///<Text formatting for the hint
    };
    std::vector<ItemData> items; ///<The hint visuals for the hints in csh->Hints (in that order)
    TextureID empty_graphics;
};

/** Functions (potentially) opening modal popups will return this.*/
enum class StatusRet {
    Ongoing,   ///<The modal popup is still open, come back in the next frame
    Cancelled, ///<The operation has been cancelled, the popup closed, don't come back next frame
    Completed  ///<The operation has been completed, the popup closed (or never opened), don't come back next frame
};

/** The settings (some of them saved) */
struct SettingsData {
    bool pedantic = false;      ///<Setting: The pedantic setting
    bool show_page_breaks = true;///<Setting: If we show dotted lines for page breaks for a multi-page chart when showing all pages
    bool technical_info = false;///<Setting: Show technical info among errors
    bool warnings = true;       ///<Setting: Show warnings among errors
    bool instant_compile = true;///<Setting: Instant compilation
    std::string forced_layout;  ///<Setting: What layout algorithm to force
    std::string forced_design;  ///<Setting: What chart design to force
    bool do_fit_window = false; ///<Setting: After compilation fit to window
    bool do_fit_width = false;  ///<Setting: After compilation fit to window width
    bool show_controls = true;  ///<Setting: if element controls are shown or not
    int filter_hints = (int)EHintFilter::Substr; ///<Setting: how do we filter hints
    float editor_font_scale = 1;   ///<Sizing the editor font
    std::chrono::milliseconds last_csh_time = std::chrono::milliseconds::zero();      ///<How long does a re-coloring take
    std::chrono::milliseconds last_compile_time = std::chrono::milliseconds::zero();  ///<How long does a recompilation last
};

SettingsData Settings;

/** A file we open, or store as recently opened. */
struct RecentFile {
    std::string path;  ///<The full UTF-8 path of the file. May use forward or backward slashes depending on OS. Empty if the file has not been saved.
    std::string name;  ///<The filename part of the path above, still in UTF-8, but without slashes. "Untitled.block" or similar. If empty, we have no file or if we denote a recently used directory
    bool operator ==(const RecentFile& o) const noexcept { return path==o.path; }
    void clear() noexcept { path.clear(); name.clear(); }
    void set(std::string_view full_path) { path = full_path; name = FileNameFromPath(full_path); }
};

/** Contains all info needed by an editor+compiled chart combo
 * Some settings are local to this, some are global & stored in ConfigData.
 * This entity cannot load/save or spawn dialogs, but can do entity
 * rename, search/replace, hinting, compilation, coloring.*/
struct EditorChartData {
    static constexpr float min_zoom = 0.01f, max_zoom = 10.f;
    static constexpr float min_editor_scale = 0.2f, max_editor_scale = 5;
    //File and editor
    unsigned slide = 0;            ///<When part of a PPT, this is the slide number
    unsigned chart = 0;            ///<When part of a PPT, this is the chart number inside the slide
    TextEditor editor;             ///< The text editor instance
    TextEditor::UndoBufferIndex compiled_at; ///< Compiled at this undo buffer index
    TextEditor::UndoBufferIndex saved_at;  ///< Saved at this undo buffer index
    //Compiled chart
    const LanguageData* lang = nullptr;///<Our current language NULL only if we have not yet opened any file here.
    std::optional<bool> pedantic;      ///<The pedantic setting for this chart, if set. Else use Settings.pedantic. (We force pedantic off for examples)
    CompileData compile_data;      ///<Holds state during a compilation.
    std::unique_ptr<Chart> pChart; ///<the currently compiled chart
    int main_file_no = 0;          ///<The number of the currently edited file in pChart->Errors.Files
    int selected_error = -1;       ///<The currently selected error
    HintData hint;                 ///<States related to the hint window

    /** Holds a texture and its size. */
    struct Texture {
        TextureID texture; ///<The backend specific texture ID
        ImVec2 size;       ///<The size of the texture bitmap in pixels (already zoomed to the current zoom)
    };
    /** Holds the compiled chart and all the element control textures.
     * Both are zoomed to the current zoom.*/
    struct Cache {
        std::optional<Texture> chart;                            ///<The chart drawn at the current zoom
        std::array<std::optional<Texture>, int(EGUIControlType::MAX)> controls; //The user controls drawn at the current zoom
    };
    Cache compile_cache; ///<The compiled, zoomed textures we display.
    /** A tracking outline/rectangle */
    struct Overlay {
        ElementRef element;    ///< The element this overlay represents
        float fade;            ///< animation status: the alpha channel to use at this moment
        bool do_fade_out;      ///< if true, we will slowly fade out
        ImVec2 offset;         ///< The offset from which this texture is to be shown (in the current zoom)
        std::optional<Texture> texture; ///< The texture to show (in the current zoom)
        Overlay(const ElementRef &e, float f, bool d) noexcept : element(e), fade(between(f, 0.f, 1.f)), do_fade_out(d) {};
    };
    std::vector<Overlay> overlays; ///<Current list of overlays to show. These are used at tracking mode or when a chart element is clicked.
    /** An element having user controls */
    struct Control {
        ElementRef element; ///< The element the controls belong to
        float size;         ///<[0.01..1] animation status. 1=fully shown
    };
    std::vector<Control> controls; ///<The element controls currently showing

    //GUI state
    std::string control_state;     ///<The collapse/expand status coming from the GUI
    float zoom = 1;                ///<The current zoom level
    unsigned page = 0;             ///<The current page shown (0 is all)
    bool tracking_mode = false;    ///<Setting: if tracking mode is on
    bool skip_next_space = false;  ///<The next space character shall be ignored (after a Ctrl+Space, when the hint box does not open)
    bool search_hoovered = false;  ///<if the search window was hoovered in the previous frame (avoid tracking)
    float tracking_mode_fade = 0;  ///<The fade status of tracking mode, we are in tracking mode if nonzero
    bool hint_window_shown = false;///<True if the Hint window is showing
    int selected_hint = -1;        ///<Hint selected
    int highlighted_hint = -1;     ///<Hint hoovered by the mouse
    int described_hint = -1;       ///<hint we show the description for
    float max_hint_height;         ///<that of the hint list box
    bool refocus_search_window = false; ///<User wants to open the search dialog. Reset this at the beginning of every frame
    bool do_open_rename = false;        ///<User wants to open the Rename dialog. Reset this at the beginning of every frame
    bool shift_focus_to_editor = false; ///<Do what it says. Apply & reset it whenever the editor window is displayed.
    float space_for_errors = 0;         ///<The status of the animation opening/closing the error pane.
    ImVec2 scrolling = {0.0f, 0.0f};    ///<Current scrolling position
    float heading_fade = 0;             ///<Status of the auto-Heading animation
    float compile_fade = 0;             ///<Status of the compile animation effect
    float R1 = 0;                       ///<Status of the rotating compile progress effect
    bool fitted_to_window = false;      ///<Set so that we can enable/disable Fit to Window button.
    bool fitted_to_width = false;       ///<Set so that we can enable/disable Fit to Width button.
    TextEditor::Coordinates saved_cursor_pos; ///<The cursor pos saved during tracking mode TODO: save full selection
    const Element* cursor_saved_for = nullptr; ///<Set if we have saved the editor cursor pos and highlight the text of an Element

    void Init(const CshParams& csh_params);
    void PreFrame() noexcept {
        refocus_search_window = false;
        do_open_rename = false;
        shift_focus_to_editor = false;
    }
    void SetFonts() {
        editor.SetFonts(GetFont(false, false, false), GetFont(false, true, false),
                        GetFont(false, false, true), GetFont(false, true, true), MscGenEditorFontSize*Settings.editor_font_scale,
                        GetFont(true, false, false));
        editor.mScale = Settings.editor_font_scale;
    }
    void SetFontScale(float scale) {
        Settings.editor_font_scale = between(scale, min_editor_scale, max_editor_scale);
        editor.mFontSize = MscGenEditorFontSize * Settings.editor_font_scale;
        editor.mScale = Settings.editor_font_scale;
    }
    bool IsDirty() const noexcept { return lang && saved_at != editor.GetUndoBufferIndex(); }

    static int SmartIndentStatic(void* data, TextEditor::Lines& lines, char character, bool shift,
                                 const TextEditor::Coordinates& from, const TextEditor::Coordinates& till) {
        return ((EditorChartData*)data)->SmartIndent(lines, character, shift, from, till);
    }
    int SmartIndent(TextEditor::Lines& lines, char character, bool shift,
                    const TextEditor::Coordinates& from, const TextEditor::Coordinates& till);
    static void ColorizeStatic(TextEditor::Lines& lines, void* data) { return ((EditorChartData*)data)->Colorize(lines); }
    void Colorize(TextEditor::Lines& lines);
    bool ProcessHints(Csh& csh);

    void ResetToNew(const std::string& text);
    void ResetText(const LanguageData* l, const std::string* text = nullptr);
    void StartCompile(); //Stops current compilation (blocks until done) & starts a new one. Returns true if we could start.
    bool CompileIfNeeded(); //Starts compilation if the chart is not up-to-date and we are not compiling now. Returns true if the chart is up-to-date. Expected to be called in every frame.
    bool CompileTakeResult(bool block); //return true if a new chart has been successfully compiled
    bool IsCompiled() const noexcept { return compiled_at == editor.GetUndoBufferIndex(); }
    bool Destroy() { return compile_data.Destroy(); }
    void DrawCache(); //(re)draw the cache parts that are invalid - leave valid ones intact
    void SetZoom(float z) { if (z==zoom) return; zoom = z; ResetCompileCache(); };
    float fit_to_window_zoom(const ImVec2& canvas) const noexcept { return (float)std::min(canvas.x/pChart->GetTotal().x.Spans(), canvas.y/(pChart->GetTotal().y.Spans() + pChart->GetCopyrightTextHeight())); }
    float fit_to_width_zoom(const ImVec2& canvas) const noexcept { return float(canvas.x/pChart->GetTotal().x.Spans()); }
    void FitToWindow(const ImVec2& canvas) noexcept { if (pChart && pChart->GetTotal().x.Spans() && pChart->GetTotal().y.Spans()) SetZoom(fit_to_window_zoom(canvas)); }
    void FitToWidth(const ImVec2& canvas) noexcept { if (pChart && pChart->GetTotal().x.Spans() && pChart->GetTotal().y.Spans()) SetZoom(fit_to_width_zoom(canvas)); }
    bool IsFittedToWindow(const ImVec2& canvas) const noexcept { return !pChart || !pChart->GetTotal().x.Spans() || !pChart->GetTotal().y.Spans() || fit_to_window_zoom(canvas)==zoom; } //returns true if chart not compiled
    bool IsFittedToWidth(const ImVec2& canvas) const noexcept { return !pChart || !pChart->GetTotal().x.Spans() || !pChart->GetTotal().y.Spans() || fit_to_width_zoom(canvas)==zoom; } //returns true if chart not compiled
    void Animate(const ElementRef &element, float initial_fade = 1, bool do_fade_out = true);
    void StartFadeOut(const Element* element) noexcept { if (Overlay* o = GetAnimation(element)) o->do_fade_out = true; }
    const Overlay* GetAnimation(const Element* element) const { auto i = std::ranges::find_if(overlays, [element](const Overlay& o) {return o.element.element==element; }); return i !=overlays.end() ? &*i : nullptr; }
    Overlay* GetAnimation(const Element* element) { auto i = std::ranges::find_if(overlays, [element](const Overlay& o) {return o.element.element==element; }); return i !=overlays.end() ? &*i : nullptr; }
    void PruneAnimations();
    void ShowControls(const ElementRef& element);
    void ControlClicked(const ElementRef& element, EGUIControlType t);
    void PostEditorRender(const TextEditor::UndoBufferIndex& undo_status_before);
    void GotoError() {
        if (!pChart || selected_error<0) return;
        const ErrorElement* E = pChart->Error.GetError((unsigned)selected_error, Settings.warnings, Settings.technical_info);
        if (E->relevant_line.file>=0 && E->relevant_line.line>=1 && E->relevant_line.col>=1)
            editor.SetCursorPosition(TextEditor::Coordinates(E->relevant_line.line-1, E->relevant_line.col-1));
    }
    void NavigateErrors(bool up, bool detailed) noexcept;
    float GetHeadingSize() noexcept;
    //returns a mouse position in chart space
    std::optional<XY> MouseToChart(const ImVec2& mouse) const noexcept;
    //returns what chart element is under the mouse.
    //mouse shall be the coordinate on the cache canvas (not yet normalized to chart space)
    ElementRef GetArc(const ImVec2& mouse) const;
    ElementRef GetArc(const TextEditor::Coordinates& pos) const;
    bool SetCursorTo(const ElementRef &E); //returns true if we have changed the cursor

    /** Starts hinting due to Ctrl+Space */
    void HintStart() {
        hint.active = true; //So that re-color does not change settings
        hint.restart_hint = false;
        hint.is_user_requested = true;
        hint.search_till_cursor = true;
        hint.replace_till_cursor = false; //replace the whole word, we have pressed ctrl+Space in.
        editor.ReColor();
        if (!hint.active)
            HintCancel(true);
    }
    void HintCancel(bool force);
    void HintSubstitute(const CshHint* hint);
    void ResetCompileCache();
    bool HandleKeyboardShortcuts();

    /** Displays the editor, handles hinting, coloring and entity rename.
     * @param [in] editor_name The name (=ID) used in the editor ImGui window.*/
    void DoEditor(const char* editor_name);
    /** Displays the chart, zooms, scrolls, tracking mode, auto-heading,
     * compilation animation.
     * @returns image_viewport_origin, and its full_canvas_size.*/
    std::pair<ImVec2, ImVec2> //
        DoChart(bool show_errors, bool fit_to_window, bool fit_to_width, bool allow_track_change);
    /* Shows & handles the search window - mostly uses global data in Config.*/
    void DoSearchReplace(const ImVec2& image_viewport_origin, const ImVec2& full_canvas_sz);
};

/** Global state and settings. */
struct ConfigData {
    using clock = std::chrono::steady_clock;
    const GUIInit *G = nullptr;    ///<The configuration we are opened with
    std::vector<std::string> Docs; ///<The documentation files, if any.
    float dpi_mul = 1.;            ///<The current screen DPI multiplier.

    std::string FullChartType(std::string_view short_name, bool empty_if_unknown=false) {
        if (!G) unknown: return empty_if_unknown ? "" : StrCat("chart of type unknown to me: ", short_name);
        const LanguageData* l = G->languages.GetLanguageByExtension(short_name);
        if (!l) goto unknown;
        return l->description;
    }

    struct ChartText {
        std::string text;
        std::string lang_str;
        TextEditor::Coordinates cursor_pos;
        unsigned slide = 0, chart = 0; //For PPT
    };
    enum class Source {
        Empty,     ///<We have nothing opened yet. Showing the Welcome Screen
        File,      ///<We have opened a text chart source (or new file)  'file' contains empty path if not yet saved to disk.
        PNG,       ///<We have opened a chart embedded in a PNG file     'file' must contain both path and name as this was opened from disk.
        PPT,       ///<We have opened chart(s) embedded in a PPT file.   'file' must contain both path and name as this was opened from disk. 'slides' contain the title of the PPT slides.
        Clipboard  ///<We have opened Art::GVML Clipformat               'file' contains both empty filename and path 'clipboard' contains the original object from the clipboard
    };
    /** Stores a session saved regularly. */
    struct RestoredState {
        RecentFile file;
        Source source;
        std::vector<ChartText> charts;
        std::vector<std::string> slides;      //for PPT
        std::string zipped_clipboard_content; //for Clipboard
    };

    std::optional<RestoredState> recovered; ///< The last session loaded with settings (if any). Offered to be re-instated on the Welcome Screen
    EditorChartData example;                ///<The editor and compiled chart for the examples file
    std::optional<Examples> examples;       ///<The loaded examples for the ExampleBrowser.

    Source source = Source::Empty;     ///<Where did the chart text originate
    RecentFile file;                   ///< The currently opened file. If path is empty, it is not saved. If name is empty we don't have a file open
    std::list<EditorChartData> charts; ///<The list of Charts we edit. We always have at least one entry. Don't use a vector, because EditorChartData is not copyable.
    std::list<EditorChartData> charts_to_delete; ///<The list of Charts which are deleted and we wait for the compilation to end. See PruneCharts();
    EditorChartData* C = nullptr;      ///<The current chart, always valid after Init(). If C->lang is null, we have not yet opened anything (Welcome screen is showing)

    std::vector<std::string> slides;   ///<One string for each slide if we have a PPT open. The strings hold the slide title or empty string if none.
    PptClipboard clipboard;            ///<If charts were opened from the clipboard, this holds the clipboard content.
    ClipboardSeen clipboard_seen;      ///<If charts were available on the clipboard, this holds their content. User may decide to open them or just drop them.

    std::string open_filter_groups;       ///< Pre-computed filter groups based on what languages we have.
    std::string window_title;             ///< Shown in the host window. Filename plus asterisk on dirty. For PPT, we also show :<slide>:<id>
    std::vector<RecentFile> recent_files; ///<Stores the recently opened files used in the File menu and Welcome Screen
    LoadDataSet load_data;                ///<Load information per language to estimate progress
private:
    mutable std::string serialized_dirs;    ///<Recent directories concatenated by ## (for ImGuiFileDialog). name1##dir1##name2##dir2...
    mutable size_t last_recent_files_size = 0; ///<When have we serialized recent dirs (what was recent_files.size(), a simple way to see changes to it, as it is mostly append only)
public:
    const std::string& GetSerializedRecentDirs() const;
    void UpdateRecentDirs(const std::string& serialized_dirs);

    std::string file_string(bool prefer_path=false, bool include_slide_chart=false) const;  ///<Return the file path, file name or "Clipboard" depending on the source of the chart. If include_slide_chart, we return z.pptx:1:2.

    bool auto_save = true;      ///<Setting: if we do automatic saves
    bool auto_heading = true;   ///<Setting: if auto heading is on
    bool auto_hint = true;      ///<Setting: if automatic hints are on.
    bool error_squiggles = true;///<Setting: If we show error squiggles
    bool smart_indent = true;   ///<Setting: If smart indent is on
    bool auto_paste_clipboard = true;///<Setting: paste eligible clipboard charts if the current chart is not dirty.
    int export_bitmap_scale_selection = 0; ///<Export Setting: how to scale bitmap images
    double export_bitmap_scale_factor = 2;  ///<Export Setting: the scale factor
    bool export_png_include_chart_text = false; ///<Export Setting: add chart text to PNG export
    bool export_ignore_pagebreaks = false; ///Export Setting: save the whole chart as one image. Not for PDF/PPT, those are governed by PPTPDFExportOptions
    std::string export_format;  ///<The last export format used
    std::string export_dir;     ///<The last export directory used
    RecentFile last_export_file;///<The last filename exported into (not saved)
    std::string last_dir;       ///<We store the last dir we used in open/save to be offered saving unnamed charts in UTF-8
    std::string settings_dir;   ///<where the settings are stored
    static constexpr std::chrono::milliseconds autosave_interval = 1s; ///<This much time of dirtiness will trigger the save.
    bool presentation_mode = false; ///<If we are in presentation mode or not
    GUIWindowGeometry main_window_geometry; ///<We store the current window state here, save & load it & use it on startup
    //PPT/PDF export options
    struct PPTPDFExportOptions {
        std::array<double, 4> margins = { 0,0,0,0 }; //margin in points
        int multipage_mode = 1; //-1 all pages to a single page (PDF only), 0: all pages in a single PPT on a single slide; 1=single PPT, one page on one slide; 2=many single-slided PPTs
        int va = 0, ha = 0; //-1=top/left; 0=center; +1=bottom/right alignment
        int unit = 1; //0 = in, 1 = cm, 2 = points
        int page_size = 0; //This means EPageSize(page_size+2), 0 is A0L
        int scale_mode = 2; //0: fit all pages, 1: fit width, 2: concrete scale
        double scale = 1; //The scale to use when export_scale_mode is 2
    };
    PPTPDFExportOptions export_ppt, export_pdf;

    bool exit_requested = false;                     ///<someone wanted us to exit
    clock::time_point ini_last_saved = clock::now(); ///<when was the setting file last saved
    clock::time_point last_autosave = clock::now();  ///<When was the file saved last time
    bool show_search = false;                        ///<Is the search window (intended to be) open

    struct Message {
        std::string message; ///<The message text
        float target = 2;
        ImU32 color = IM_COL32(0, 255, 0, 255);
        float Y = -1;        ///<The Y position of the message. if <0, we need to assign this message
        float fade = 0;      ///<The status of the message >1 means showing full, (0..1) starts fading, 0 = to be removed
    };
    std::vector<Message> messages; ///<The messages to display - used by EditorChartData
    void AddMessage(Message&& m, bool combine = false);

    void Init(const GUIInit &_G, float dpi_mul_, std::vector<std::string>&& docs, const std::string& settings_dir, const std::string& current_dir);
    void LoadSettings();
    void SaveSettings(bool force);
    //Mark all charts for deletion except 'C'
    void DestroyCharts() { for (auto i = charts.begin(); i!=charts.end(); ) if (&*i!=C) charts_to_delete.splice(charts_to_delete.end(), charts, i++); else i++; }
    void PruneCharts() { std::erase_if(charts_to_delete, [](EditorChartData& c) { return c.Destroy(); }); } //Remove charts marked as for destruction, where the compile thread has stopped.

    std::optional<RecentFile> RecentFilesAsMenu() const;
    std::optional<std::string> RecentDirsAsMenu() const;
    void SetFileDialogColors() const;
    void ResetText(const LanguageData* l, const std::string* text=nullptr);
    StatusRet New(bool do_welcome, const LanguageData* use_lang = nullptr);
    StatusRet Load(std::variant<const RecentFile*, const std::string*, ClipboardSeen*> load_this = static_cast<const std::string*>(nullptr));
    std::pair<bool, std::string> LoadPpt(const std::vector<PptFile::ChartInPPT>& ppt_charts, const RecentFile& lfile);
    StatusRet CheckOverwrite(const char *message);
    enum class SaveMode {
        As,             ///<always open a dialog to ask filename
        Interactive,    ///<Open a dialog to ask name, if no name. Display dialog on error.
        Quiet,          ///<Do not save if no filename. Do not show dialog on error.
        CopyToClipboard ///<Copy the current chart text to the clipboard as a newly created Art::GVML ClipFormat object, keep existing file/clipboard as opened (dont change 'source' and 'file')
    };
    StatusRet Save(SaveMode mode = SaveMode::Interactive);
    StatusRet Export(bool reuse_last_name);
    bool IsDirty() const noexcept { return std::ranges::any_of(charts, [](const EditorChartData& c) {return c.IsDirty(); }); }
    void UpdateWindowTitle();

    void AddRecentFile(RecentFile &&f) {
        std::string dir(DirNameFromPath(f.path));
        std::erase(recent_files, f);
        recent_files.push_back(std::move(f));
        if (dir.size()) last_dir = std::move(dir);
    }
    void DelRecentFile(const RecentFile &fn) {
        std::erase_if(recent_files, [&fn](const RecentFile &r) { return r.path==fn.path; });
    }
    GUIReturn Ret(bool focus);
};

ConfigData Config; ///<The singleton having GUI state

//Extract up until the first zero
//display error and return empty if text is empty.
//Move 'text' to the char after the zero.
std::optional<std::string_view> extract(std::string_view &text, const std::string &settings_dir) {
    if (text.empty()) {
        std::cerr << "File '" << settings_dir << MSC_GEN_INI_FILE_NAME << "' is too short. Using what I can."<< std::endl;
        return {};
    }
    const size_t pos = text.find('\0');
    std::string_view ret;
    if (pos == text.npos) { ret = text; text.remove_prefix(text.size()); }
    else { ret = text.substr(0, pos); text.remove_prefix(pos+1); }
    return ret;
}

#define MSC_GENERATOR_INI_INTRO "Msc-generator ini file\n"

void ConfigData::LoadSettings() {
    const std::string in_name = settings_dir + MSC_GEN_INI_FILE_NAME;
    FILE *in = G->file_open_proc(in_name.c_str(), false, true); //true=binary Keep \n on Windows, too.
    if (!in) return;
    std::string stext = ReadFile(in);
    fclose(in);
    export_ppt.page_size = unsigned(DEFAULT_PPT_PAGE_SIZE) - unsigned(EPageSize::NO_PAGE) - 1; //PPT default slide size is Widescreen landscape
    export_pdf.page_size = unsigned(EPageSize::A4P) - unsigned(EPageSize::NO_PAGE) - 1;        //PDF default page size is A4 portrait
    std::string_view text = stext;
    if (!text.starts_with(MSC_GENERATOR_INI_INTRO)) {
        std::cerr << "File '" << settings_dir << MSC_GEN_INI_FILE_NAME << "' does not seem to be an Msc-generator ini file. Ignoring it."<< std::endl;
        return;
    }
    text.remove_prefix(strlen(MSC_GENERATOR_INI_INTRO));
    if (auto ini = extract(text, settings_dir))
        ImGui::LoadIniSettingsFromMemory(ini->data(), ini->size());
    //First byte is version
    if (text.empty() || text.front()!='0') {
        std::cerr << "File '" << settings_dir << MSC_GEN_INI_FILE_NAME << "' is of a newer version that I support. Ignoring some of it."<< std::endl;
        return;
    }
    text.remove_prefix(1);
    //Now come the newline terminated recent file list, terminated by a zero
    while (text.size() && text.front()!='\0') {
        RecentFile r;
        size_t pos = text.find('\n');
        if (pos == text.npos) pos = text.size();
        r.path.assign(text.substr(0, pos));
        text.remove_prefix(std::min(pos+1, text.length()));
        pos = text.find('\n');
        if (pos == text.npos) pos = text.size();
        r.name.assign(text.substr(0, pos));
        text.remove_prefix(std::min(pos+1, text.length()));
        recent_files.push_back(std::move(r));
    }
    if (text.size()) text.remove_prefix(1); //swallow terminating zero
    if (auto e = extract(text, settings_dir); !e) return;
    else Settings.pedantic = *e=="1";
    if (auto e = extract(text, settings_dir); !e) return;
    else Settings.technical_info = *e=="1";
    if (auto e = extract(text, settings_dir); !e) return;
    else Settings.warnings = *e=="1";
    if (auto e = extract(text, settings_dir); !e) return;
    else Settings.instant_compile = *e=="1";
    if (auto e = extract(text, settings_dir); !e) return;
    else Settings.do_fit_width = *e=="1", Settings.do_fit_window = *e=="2";
    if (auto e = extract(text, settings_dir); !e) return;
    else auto_heading = *e=="1";
    if (auto e = extract(text, settings_dir); !e) return;
    else auto_hint = *e=="1";
    if (auto e = extract(text, settings_dir); !e) return;
    else if (double s; from_chars(*e, s)) std::cerr<<"Could not parse editor scale from '"<<*e<<"'. Ignoring this setting."<<std::endl;
    else Settings.editor_font_scale = between(float(s), EditorChartData::min_editor_scale, EditorChartData::max_editor_scale);
    if (auto e = extract(text, settings_dir); !e) return;
    else error_squiggles = *e=="1";
    if (auto e = extract(text, settings_dir); !e) return;
    else smart_indent = *e=="1";
    if (auto e = extract(text, settings_dir); !e) return;
    else if (from_chars(*e, G->languages.cshParams->m_II)) std::cerr<<"Could not parse instruction indent from '"<<*e<<"'. Ignoring this setting."<<std::endl;
    if (auto e = extract(text, settings_dir); !e) return;
    else if (from_chars(*e, G->languages.cshParams->m_IB)) std::cerr<<"Could not parse block indent from '"<<*e<<"'. Ignoring this setting."<<std::endl;
    if (auto e = extract(text, settings_dir); !e) return;
    else if (from_chars(*e, G->languages.cshParams->m_IM)) std::cerr<<"Could not parse middle indent from '"<<*e<<"'. Ignoring this setting."<<std::endl;
    if (auto e = extract(text, settings_dir); !e) return;
    else if (from_chars(*e, G->languages.cshParams->m_IA)) std::cerr<<"Could not parse attribute indent from '"<<*e<<"'. Ignoring this setting."<<std::endl;
    if (auto e = extract(text, settings_dir); !e) return;
    else G->languages.cshParams->m_bSIText = *e=="1"; //special indent for labels
    if (auto e = extract(text, settings_dir); !e) return;
    else G->languages.cshParams->m_bSIAttr = *e=="1"; //special indent for attributes
    if (auto e = extract(text, settings_dir); !e) return;
    else if (int s;  from_chars(*e, s)) std::cerr<<"Could not parse color_scheme from '"<<*e<<"'. Ignoring this setting."<<std::endl;
    else G->languages.cshParams->color_scheme = between(s, 0, 3);
    //Settings introduced in v7.1
    if (auto e = extract(text, settings_dir); !e) return;
    else Settings.show_controls = *e=="1";
    if (auto e = extract(text, settings_dir); !e) return;
    else auto_save = *e=="1";
    //Settings introduced in v7.2
    if (auto e = extract(text, settings_dir); !e) return;
    else {
        recovered.reset();
        if (int no_charts; !from_chars(*e, no_charts)) {
            if (auto n = extract(text, settings_dir); !n) return; //filename
            else if (auto p = extract(text, settings_dir); !p) return; //filepath
            else {
                recovered.emplace(RecentFile{ std::string(*p), std::string(*n) });
                while (no_charts--) {
                    if (auto t = extract(text, settings_dir); !t) return; //chart text
                    else if (auto la = extract(text, settings_dir); !la) return; //language string
                    else if (auto l = extract(text, settings_dir); !l) return;  //cursor line
                    else if (auto c = extract(text, settings_dir); !c) return;  //cursor column
                    else if (auto pp = extract(text, settings_dir); !pp) return;  //ppt slide
                    else if (auto pc = extract(text, settings_dir); !pc) return;  //ppt chart
                    else if (auto new_lang = G->languages.GetLanguage(*la); !new_lang)
                        std::cerr<<"The last session had a language '"<<*la<<"' I do not have. Ignoring last session."<<std::endl;
                    else {
                        recovered->charts.emplace_back(std::string(*t), std::string(*la));
                        if (from_chars(*l, recovered->charts.back().cursor_pos.mLine)
                                || from_chars(*c, recovered->charts.back().cursor_pos.mColumn)) {
                            std::cerr<<"Could not parse cursor_pos from '"<<*l<<"' or '"<<*c<<"'. Ignoring this setting."<<std::endl;
                            recovered->charts.back().cursor_pos = { 0,0 };
                        }
                        int slide = 0, chart = 0;
                        if (from_chars(*pp, slide) || slide<0
                                || from_chars(*pc, chart) || chart<0)
                            std::cerr<<"Could not parse slide/chart from '"<<*pp<<"' or '"<<*pc<<"'. Ignoring this setting."<<std::endl;
                        recovered->charts.back().slide = slide;
                        recovered->charts.back().chart = chart;
                    }
                }
            }
        }
    }
    //Settings introduced in v7.3
    if (auto e = extract(text, settings_dir); !e) return;
    else Settings.show_page_breaks = *e=="1";
    if (auto e = extract(text, settings_dir); !e) return;
    else if (from_chars(*e, export_bitmap_scale_selection)) std::cerr<<"Could not parse export_bitmap_scale_selection from '"<<*e<<"'. Ignoring this setting."<<std::endl;
    if (auto e = extract(text, settings_dir); !e) return;
    else if (double d;  from_chars(*e, d)) std::cerr<<"Could not parse export_bitmap_scale_factor from '"<<*e<<"'. Ignoring this setting."<<std::endl;
    else export_bitmap_scale_factor = (float)d;
    if (auto e = extract(text, settings_dir); !e) return;
    else export_png_include_chart_text = *e=="1";
    if (auto e = extract(text, settings_dir); !e) return;
    else export_ignore_pagebreaks = *e=="1";
    if (auto e = extract(text, settings_dir); !e) return;
    else export_format = *e;
    if (auto e = extract(text, settings_dir); !e) return;
    else export_dir = *e;
    main_window_geometry = {};
    if (auto e = extract(text, settings_dir); !e) return;
    else if (from_chars(*e, main_window_geometry.cx)) std::cerr<<"Could not parse main_window_size.cx from '"<<*e<<"'. Ignoring this setting."<<std::endl;
    if (auto e = extract(text, settings_dir); !e) return;
    else if (from_chars(*e, main_window_geometry.cy)) std::cerr<<"Could not parse main_window_size.cy from '"<<*e<<"'. Ignoring this setting."<<std::endl;
    if (auto e = extract(text, settings_dir); !e) return;
    else if (from_chars(*e, main_window_geometry.x)) std::cerr<<"Could not parse main_window_size.cx from '"<<*e<<"'. Ignoring this setting."<<std::endl;
    if (auto e = extract(text, settings_dir); !e) return;
    else if (from_chars(*e, main_window_geometry.y)) std::cerr<<"Could not parse main_window_size.cy from '"<<*e<<"'. Ignoring this setting."<<std::endl;
    if (auto e = extract(text, settings_dir); !e) return;
    else if (int s;  from_chars(*e, s)) std::cerr<<"Could not parse filter_hints from '"<<*e<<"'. Ignoring this setting."<<std::endl;
    else Settings.filter_hints = between(s, 0, int(EHintFilter::Max));
    if (auto e = extract(text, settings_dir); !e) return;
    else last_dir = *e;
    for (auto* fmt :{ &export_ppt, &export_pdf }) {
        for (double& d : fmt->margins)
            if (auto e = extract(text, settings_dir); !e) return;
            else if (from_chars(*e, d)) std::cerr<<"Could not parse export margin from '"<<*e<<"'. Ignoring this setting."<<std::endl;
        if (auto e = extract(text, settings_dir); !e) return;
        else if (from_chars(*e, fmt->multipage_mode)) std::cerr<<"Could not parse multipage export from '"<<*e<<"'. Ignoring this setting."<<std::endl;
        if (auto e = extract(text, settings_dir); !e) return;
        else if (from_chars(*e, fmt->va)) std::cerr<<"Could not parse export vertical alignment from '"<<*e<<"'. Ignoring this setting."<<std::endl;
        else if (fmt->va<-1 || +1<fmt->va) fmt->va = 0, std::cerr<<"Bad export vertical alignment '"<<*e<<"'. Using 0."<<std::endl;
        if (auto e = extract(text, settings_dir); !e) return;
        else if (from_chars(*e, fmt->ha)) std::cerr<<"Could not parse export hortizontal alignment from '"<<*e<<"'. Ignoring this setting."<<std::endl;
        else if (fmt->ha<-1 || +1<fmt->ha) fmt->ha = 0, std::cerr<<"Bad export horizontal alignment '"<<*e<<"'. Using 0."<<std::endl;
        if (auto e = extract(text, settings_dir); !e) return;
        else if (from_chars(*e, fmt->unit)) std::cerr<<"Could not parse export measurement unit from '"<<*e<<"'. Ignoring this setting."<<std::endl;
        else if (fmt->unit<0 || 2<fmt->unit) fmt->unit = 1, std::cerr<<"Bad export measurement unit '"<<*e<<"'. Using 1."<<std::endl;
        if (auto e = extract(text, settings_dir); !e) return;
        else if (from_chars(*e, fmt->scale)) std::cerr<<"Could not parse export scale from '"<<*e<<"'. Ignoring this setting."<<std::endl;
        if (auto e = extract(text, settings_dir); !e) return;
        else if (from_chars(*e, fmt->scale_mode)) std::cerr<<"Could not parse export scale mode from '"<<*e<<"'. Ignoring this setting."<<std::endl;
    }
    //Settings introduced in v8.0
    if (recovered) {
        recovered->source = Source::File;
        if (auto e = extract(text, settings_dir); !e) return;
        else if (int src; from_chars(*e, src) || src<0 || src>4)
            std::cerr<<"Could not parse source from '"<<*e<<"'. Ignoring this setting."<<std::endl;
        else
            recovered->source = Source(src);
        if (auto e = extract(text, settings_dir); !e) return;
        else if (int no_slides; from_chars(*e, no_slides) || no_slides<0)
            std::cerr<<"Could not parse number of slides from '"<<*e<<"'. Ignoring this setting."<<std::endl;
        else while(no_slides--) {
            if (auto s = extract(text, settings_dir); !s) return;
            else recovered->slides.push_back(std::string(*s));
        }
        //zipped content may contain 0
        if (auto e = extract(text, settings_dir); !e) return;
        else if (int len; from_chars(*e, len) || len<0)
            std::cerr<<"Could not parse zipped len from '"<<*e<<"'. Ignoring this setting."<<std::endl;
        else if (len>(int)text.size()) {
            std::cerr<<"Could not parse zipped from what remains len="<<len<<" remains="<<text.size()<<std::endl;
            return;
        } else {
            recovered->zipped_clipboard_content.assign(text.substr(0, len));
            text.remove_prefix(len);
        }
    }
    if (auto e = extract(text, settings_dir); !e) return;
    else auto_paste_clipboard = *e=="1";
    //Leave remainder of settings file unloaded (forward compatibility)

    //If the user specified pedantic or forced design on the command-line
    if (G->pedantic)
        Settings.pedantic = *G->pedantic;
    if (G->forced_design.size())
        Settings.forced_design = G->forced_design;
}

void ConfigData::SaveSettings(bool force) {
    ImGuiIO &io = ImGui::GetIO();
    if (!io.WantSaveIniSettings && !force && clock::now()-ini_last_saved<10s)
        return;
    io.WantSaveIniSettings = false;
    ini_last_saved = clock::now();

    const std::string out_name = settings_dir + MSC_GEN_INI_FILE_NAME ".tmp";
    FILE *out = G->file_open_proc(out_name.c_str(), true, true); //true=binary Keep \n on Windows, too.
    static int error_reported = 0;
    constexpr int max_error_report = 3;
    if (!out) {
        if (error_reported<max_error_report) {
            std::cerr<< "Unable to write to settings file '" + settings_dir << MSC_GEN_INI_FILE_NAME ".tmp" << "': " << my_strerror(errno) << std::endl;
            if (++error_reported == max_error_report)
                std::cerr<< "I will not report further settings file save problems." << std::endl;
        }
        return;
    } else
        error_reported = 0;
    fprintf(out, MSC_GENERATOR_INI_INTRO); //intro
    size_t imgui_ini_len;
    const char *const imgui_ini_text = ImGui::SaveIniSettingsToMemory(&imgui_ini_len);
    fwrite(imgui_ini_text, 1, imgui_ini_len, out);
    fputc('\0', out);
    fputc('0', out); //version
    std::string config;
    for (const RecentFile &r : recent_files) {
        config.append(r.path).push_back('\n');
        config.append(r.name).push_back('\n');
    }
    config.push_back('\0');
    fwrite(config.data(), 1, config.size(), out);

    if (Settings.pedantic) fputc('1', out);
    fputc('\0', out);
    if (Settings.technical_info) fputc('1', out);
    fputc('\0', out);
    if (Settings.warnings) fputc('1', out);
    fputc('\0', out);
    if (Settings.instant_compile) fputc('1', out);
    fputc('\0', out);
    if (Settings.do_fit_width) fputc('1', out);
    else if (Settings.do_fit_window) fputc('2', out);
    fputc('\0', out);
    if (auto_heading) fputc('1', out);
    fputc('\0', out);
    if (auto_hint) fputc('1', out);
    fputc('\0', out);
    fprintf(out, "%f", Settings.editor_font_scale);
    fputc('\0', out);
    if (error_squiggles) fputc('1', out);
    fputc('\0', out);
    if (smart_indent) fputc('1', out);
    fputc('\0', out);
    fprintf(out, "%d", G->languages.cshParams->m_II);
    fputc('\0', out);
    fprintf(out, "%d", G->languages.cshParams->m_IB);
    fputc('\0', out);
    fprintf(out, "%d", G->languages.cshParams->m_IM);
    fputc('\0', out);
    fprintf(out, "%d", G->languages.cshParams->m_IA);
    fputc('\0', out);
    if (G->languages.cshParams->m_bSIText) fputc('1', out);
    fputc('\0', out);
    if (G->languages.cshParams->m_bSIAttr) fputc('1', out);
    fputc('\0', out);
    fprintf(out, "%d", G->languages.cshParams->color_scheme);
    fputc('\0', out);
    //Settings introduced in v7.1
    if (Settings.show_controls) fputc('1', out);
    fputc('\0', out);
    if (auto_save) fputc('1', out);
    fputc('\0', out);
    //Settings introduced in v7.2
    if (IsDirty()) {
        fprintf(out, "%u", (unsigned)charts.size()); fputc('\0', out);
        fprintf(out, "%s", file.name.c_str());    fputc('\0', out);
        fprintf(out, "%s", file.path.c_str());    fputc('\0', out);
        for (const EditorChartData& c: charts) {
            fprintf(out, "%s", c.editor.GetText().c_str());           fputc('\0', out);
            fprintf(out, "%s", c.lang->GetName().c_str());            fputc('\0', out);
            fprintf(out, "%d", c.editor.GetCursorPosition().mLine);   fputc('\0', out);
            fprintf(out, "%d", c.editor.GetCursorPosition().mColumn); fputc('\0', out);
            fprintf(out, "%d", c.slide);                              fputc('\0', out);
            fprintf(out, "%d", c.chart);                              fputc('\0', out);
        }
    } else if (file.path.empty() && recovered) {
        fprintf(out, "%u", (unsigned)recovered->charts.size()); fputc('\0', out);
        fprintf(out, "%s", recovered->file.name.c_str());       fputc('\0', out);
        fprintf(out, "%s", recovered->file.path.c_str());       fputc('\0', out);
        for (const ChartText& c : recovered->charts) {
            fprintf(out, "%s", c.text.c_str());       fputc('\0', out);
            fprintf(out, "%s", c.lang_str.c_str());   fputc('\0', out);
            fprintf(out, "%d", c.cursor_pos.mLine);   fputc('\0', out);
            fprintf(out, "%d", c.cursor_pos.mColumn); fputc('\0', out);
            fprintf(out, "%d", c.slide);              fputc('\0', out);
            fprintf(out, "%d", c.chart);              fputc('\0', out);
        }
    } else
        fputc('\0', out);
    //Settings introduced in v7.3
    if (Settings.show_page_breaks) fputc('1', out);
    fputc('\0', out);
    fprintf(out, "%d", export_bitmap_scale_selection);
    fputc('\0', out);
    fprintf(out, "%g", export_bitmap_scale_factor);
    fputc('\0', out);
    if (export_png_include_chart_text) fputc('1', out);
    fputc('\0', out);
    if (export_ignore_pagebreaks) fputc('1', out);
    fputc('\0', out);
    fprintf(out, "%s", export_format.c_str());
    fputc('\0', out);
    fprintf(out, "%s", export_dir.c_str());
    fputc('\0', out);
    fprintf(out, "%d", main_window_geometry.cx);
    fputc('\0', out);
    fprintf(out, "%d", main_window_geometry.cy);
    fputc('\0', out);
    fprintf(out, "%d", main_window_geometry.x);
    fputc('\0', out);
    fprintf(out, "%d", main_window_geometry.y);
    fputc('\0', out);
    fprintf(out, "%d", Settings.filter_hints);
    fputc('\0', out);
    fprintf(out, "%s", last_dir.c_str());
    fputc('\0', out);
    for (auto* fmt :{ &export_ppt, &export_pdf }) {
        for (double d : fmt->margins) {
            fprintf(out, "%g", d);
            fputc('\0', out);
        }
        fprintf(out, "%d", fmt->multipage_mode); _ASSERT(-1<=fmt->multipage_mode && fmt->multipage_mode<=2);
        fputc('\0', out);
        fprintf(out, "%d", fmt->va); _ASSERT(-1<=fmt->va && fmt->va<=+1);
        fputc('\0', out);
        fprintf(out, "%d", fmt->ha); _ASSERT(-1<=fmt->ha && fmt->ha<=+1);
        fputc('\0', out);
        fprintf(out, "%d", fmt->unit); _ASSERT(0<=fmt->unit && fmt->unit<=2);
        fputc('\0', out);
        fprintf(out, "%g", fmt->scale);
        fputc('\0', out);
        fprintf(out, "%d", fmt->scale_mode); _ASSERT(0<=fmt->scale_mode && fmt->scale_mode<=2);
        fputc('\0', out);
    }
    //Settings introduced in v8.0
    if (IsDirty()) {
        fprintf(out, "%d", (int)source); _ASSERT(0<=int(source) && (int)source<=4);
        fputc('\0', out);
        fprintf(out, "%d", source==Source::PPT ? (int)slides.size() : 0);
        fputc('\0', out);
        if (source==Source::PPT)
            for (const std::string& s : slides) {
                fprintf(out, "%s", s.c_str());
                fputc('\0', out);
            }
        const size_t clipboard_content_size = source==Source::Clipboard ? clipboard.GetZippedContent().size() : 0;
        fprintf(out, "%d", (int)clipboard_content_size);
        fputc('\0', out);
        if (clipboard_content_size)
            fwrite(clipboard.GetZippedContent().data(), clipboard_content_size, 1, out);
    } else if (file.path.empty() && recovered) {
        fprintf(out, "%d", (int)recovered->source); _ASSERT(0 <= int(recovered->source) && (int)recovered->source <= 4);
        fputc('\0', out);
        fprintf(out, "%d", recovered->source == Source::PPT ? (int)recovered->slides.size() : 0);
        fputc('\0', out);
        if (recovered->source == Source::PPT)
            for (const std::string& s : recovered->slides) {
                fprintf(out, "%s", s.c_str());
                fputc('\0', out);
            }
        const size_t clipboard_content_size = recovered->source == Source::Clipboard ? recovered->zipped_clipboard_content.size() : 0;
        fprintf(out, "%d", (int)clipboard_content_size);
        fputc('\0', out);
        if (clipboard_content_size)
            fwrite(recovered->zipped_clipboard_content.data(), clipboard_content_size, 1, out);
    }
    if (auto_paste_clipboard) fputc('1', out);
    fputc('\0', out);
    fclose(out);

    //atomically update the saved config
    std::string err;
#ifdef WIN32
    //Note: on Windows this preserves the creation date of the ini file due to a feature called "file system tunneling"
    const bool success = ReplaceFileA((settings_dir + MSC_GEN_INI_FILE_NAME).c_str(), 
                                      (settings_dir + MSC_GEN_INI_FILE_NAME ".tmp").c_str(),
                                      nullptr, REPLACEFILE_IGNORE_MERGE_ERRORS, 0, 0);
    if (!success) {
        char buf[1024];
        FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_MAX_WIDTH_MASK,
                       nullptr, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
                       buf, sizeof(buf), nullptr);
        err = buf;
    }
#else //WIN32
    const int res = rename((settings_dir + MSC_GEN_INI_FILE_NAME ".tmp").c_str(), (settings_dir + MSC_GEN_INI_FILE_NAME).c_str());
    if (res)
        err = my_strerror(errno);
#endif // WIN32
    if (err.empty()) {
        error_reported = 0;
    } else if (error_reported < max_error_report) {
        std::cerr << "Unable to write to settings file '" + settings_dir << MSC_GEN_INI_FILE_NAME << "': " << err << std::endl;
        if (++error_reported == max_error_report)
            std::cerr << "I will not report further settings file save problems." << std::endl;
    }
}

void EditorChartData::Init(const CshParams &csh_params) {
    editor.mIndent = Config.smart_indent ? &EditorChartData::SmartIndentStatic : nullptr;
    editor.mIndentData = this;
    editor.mIndentChars = "\n\t\x08{}[]";
    editor.mColorize = &ColorizeStatic;
    editor.mColorizeData = this;
    editor.SetShowWhitespaces(false);
    SetFonts();
    editor.SetPalette(MscCshAppearanceList[csh_params.color_scheme]);
}

const std::string& ConfigData::GetSerializedRecentDirs() const {
    if (last_recent_files_size!=recent_files.size()) {
        serialized_dirs.clear();
        std::unordered_set<std::string> added;
        added.reserve(recent_files.size());
        for (auto i = recent_files.rbegin(); i!=recent_files.rend(); i++) {
            const std::string dir_path(i->name.empty() ? i->path : DirNameFromPath(i->path));
            if (dir_path.empty()) continue;
            if (added.contains(dir_path)) continue;
            added.insert(dir_path);
            serialized_dirs.append(FileNameFromPath(dir_path)).append("##").append(dir_path).append("##");
        }
        if (serialized_dirs.size()>=2) serialized_dirs.resize(serialized_dirs.size()-2); //pop trailing ##
    }
    return serialized_dirs;
}

void ConfigData::UpdateRecentDirs(const std::string& serialized_dirs) {
    std::string_view t(serialized_dirs);
    std::unordered_set<std::string_view> dirs;
    while (t.size()) {
        const size_t f = t.find_first_of('#');
        if (f==std::string::npos) break;
        t.remove_prefix(f+1);
        if (t.starts_with('#')) t.remove_prefix(1);
        if (t.empty()) break;
        const size_t s = t.find_first_of('#');
        const std::string_view path = s==std::string::npos ? t : t.substr(0, s);
        if (s!=std::string::npos) t.remove_prefix(std::min(size_t(s+2), t.size()));
        dirs.insert(path);
    }
    //Now remove dirs from RecentFiles, we don't have in 'dirs' and add the ones, we have
    std::erase_if(recent_files, [&dirs](const RecentFile& r) {
        if (r.name.size()) {
            dirs.erase(DirNameFromPath(r.path));
            return false;
        }
        const bool user_removed = !dirs.contains(r.path);
        dirs.erase(r.path);
        return user_removed;
    });
    for (std::string_view path : dirs)
        AddRecentFile(RecentFile{std::string(path), ""});
}

std::string ConfigData::file_string(bool prefer_path, bool include_slide_chart) const {
    if (source == Source::Empty) return {};
    if (source == Source::Clipboard) return "From Clipboard";
    std::string ret = prefer_path
        ? file.path.empty() ? file.name : file.path
        : file.name.empty() ? file.path : file.name;
    if (include_slide_chart && source==Source::PPT)
        ret += StrCat(":", C->slide, ":", C->chart);
    return ret;
}

void ConfigData::AddMessage(Message&& m, bool combine) {
    if (combine) {
        auto i = std::ranges::find_if(messages, [&m](const Message& msg) {return m.message==msg.message; });
        if (i!=messages.end()) {
            i->target = m.target;
            i->color = m.color;
            i->fade = std::min(i->target, i->fade);
            return;
        }
    }
    messages.push_back(std::move(m));
}

/**
* @param[in] settings_dir UTF-8 home dir spec(fw/bw slashes on Unix/windows) Used to
*            place the GUI status file(imgui_ini), and the designs and load file on Linux.
*            On Linux it defaults to $HOME/.msc-genrc, but can be overridden with the
*            MSC_GEN_RC environment variable to be $MSC_GEN_RC/.msc_genrc.
*            On Windows it is AppData\\Msc-generator.*/

void ConfigData::Init(const GUIInit &_G, float dpi_mul_, std::vector<std::string>&& docs,
                      const std::string& settings_dir_, const std::string& current_dir) {
    if (G) return;
    G = &_G;
    dpi_mul = dpi_mul_;
    Docs = std::move(docs);
    MscGenEditorFontSize = 13 * dpi_mul;
    MscGenWindowFontSize = 16 * dpi_mul;
    last_dir = export_dir = current_dir;
    settings_dir = settings_dir_;
    C = &charts.emplace_back();
    LoadFonts();
    LoadSettings();

    std::string all_exts_machine;
    for (std::string_view lname : G->languages.GetLanguages()) {
        auto l = G->languages.GetLanguage(lname);
        std::string exts_human, exts_machine;
        for (const std::string &e : l->extensions) {
            if (exts_human.empty()) exts_human = "*."+e;
            else exts_human.append(" *.").append(e);
            if (exts_machine.empty()) exts_machine = "."+e;
            else exts_machine.append(",.").append(e);
        }
        open_filter_groups.append(l->description).append("s (").append(exts_human).append("){")
                          .append(exts_machine).append("},");
        if (all_exts_machine.empty()) all_exts_machine = std::move(exts_machine);
        else all_exts_machine.append(",").append(exts_machine);
    }
    open_filter_groups.append(",PNG Embedded charts (*.png){.png}");
    open_filter_groups.append(",PPT Embedded charts (*.pptx){.pptx}");
    all_exts_machine.append(",.png,.pptx");
    open_filter_groups = "All chart types{"+all_exts_machine+"},"+open_filter_groups;

    MscError Error;
    FileLineCol opt_pos;
    load_data = SplitLoadData(G->load_data, &Error, &opt_pos);
    std::cerr << Error.Print(true, false); //nothing if no error

    C->Init(*G->languages.cshParams);
    example.Init(*G->languages.cshParams);
    example.pedantic = false;
    examples.emplace(&G->languages);

    SetFileDialogColors();

    //If the user read from the standard in.
    if (G->start_chart_text.size() && G->type.size())
        if (auto *lang = G->languages.GetLanguage(G->type))
            ResetText(lang, &G->start_chart_text);
}

void ConfigData::ResetText(const LanguageData *l, const std::string* text) {
    C->ResetText(l, text);
    file.name = "Untitled."+C->lang->extensions.front();
    file.path.clear();
    source = Source::File;
}

void Help(const char *desc) {
    if (ImGui::IsItemHovered()) {
        ImGui::SetNextWindowPos(ImGui::GetIO().MousePos + ImVec2(16, 16)); //default pos would be +16,+8
        ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4);
        ImGui::BeginTooltip();
        ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f);
        ImGui::TextUnformatted(desc);
        ImGui::PopTextWrapPos();
        ImGui::EndTooltip();
        ImGui::PopStyleVar();
    }
}
std::optional<RecentFile> ConfigData::RecentFilesAsMenu() const {
    std::optional<RecentFile> ret;
    bool had = false;
    for (auto i = recent_files.rbegin(); i!=recent_files.rend(); i++)
        if (i->name.size()) { //if only the dir was used, the 'name' is empty - do not show
            if (ImGui::MenuItem(i->name.c_str(), "", nullptr))
                ret = *i; //Do not Load here as it will screw Config.recent_files;
            Help(i->path.c_str());
            had = true;
        }
    if (!had)
        ImGui::MenuItem("...No recently opened files...", "", nullptr, false);
    return ret;
}

std::optional<std::string> ConfigData::RecentDirsAsMenu() const {
    std::optional<std::string> ret;
    bool had = false;
    std::unordered_set<std::string> displayed;
    displayed.reserve(recent_files.size());
    for (auto i = recent_files.rbegin(); i!=recent_files.rend(); i++) {
        const std::string dir_path(i->name.empty() ? i->path : DirNameFromPath(i->path));
        if (dir_path.empty()) continue;
        if (displayed.contains(dir_path)) continue;
        displayed.insert(dir_path);
        const std::string dir_name(FileNameFromPath(dir_path));
        if (ImGui::MenuItem(dir_name.c_str(), "", nullptr))
            ret = dir_path;
        Help(dir_path.c_str());
        had = true;
    }
    if (!had)
        ImGui::MenuItem("...No recent directories...", "", nullptr, false);
    return ret;
}

namespace {

/// Modal single-file dialog with a toggle for hidden files
struct MscFileDialog : ImGuiFileDialog {
    static auto Instance() { static MscFileDialog inst; return &inst; }
    void OpenDialog(const std::string& key, const std::string& title, const char* filter, const std::string& path, const std::string& fname) {
        ImGuiFileDialog::OpenDialog(key, title, filter, path, fname, 1, nullptr, ImGuiFileDialogFlags_Modal | ImGuiFileDialogFlags_DontShowHiddenFiles);
    }
protected:
    bool prDrawValidationButtons() override {
        static const char showHiddenStr[] = "Show hidden";
        auto ret = ImGuiFileDialog::prDrawValidationButtons();
        // Only show the toggle if there is enough horizontal space for it.
        // The width is calculated as in https://github.com/ocornut/imgui/issues/3714#issuecomment-759319268
        auto const toggleWidth = ImGui::GetStyle().ItemSpacing.x + ImGui::GetFrameHeight() + ImGui::GetStyle().ItemInnerSpacing.x + ImGui::CalcTextSize(showHiddenStr).x,
                   toggleOffsetX = ImGui::GetContentRegionAvail().x - toggleWidth;
        if (toggleOffsetX >= prOkCancelButtonWidth) {
            ImGui::SameLine();
            ImGui::SetCursorPosX(toggleOffsetX);
            bool showHidden = !(prFileDialogInternal.puDLGflags & ImGuiFileDialogFlags_DontShowHiddenFiles);
            ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1);
            if (ImGui::Checkbox(showHiddenStr, &showHidden)) {
                prFileDialogInternal.puDLGflags ^= ImGuiFileDialogFlags_DontShowHiddenFiles;
                prFileDialogInternal.puFileManager.OpenCurrentPath(prFileDialogInternal);
            }
            ImGui::PopStyleVar();
            if (ImGui::IsItemHovered())
                ImGui::SetTooltip("Toggle hidden");
        }
        return ret;
    }
};

}

void ConfigData::SetFileDialogColors() const {
    static constexpr std::array colors = {
        IM_COL32(92,0,0,255),
        IM_COL32(0,92,0,255),
        IM_COL32(0,0,92,255),
        IM_COL32(0,60,60,255),
        IM_COL32(60,0,60,255),
        IM_COL32(60,60,0,255),
    };
    if (!G) return;
    const auto langs = G->languages.GetLanguages();
    for (const std::string_view &lang : langs) {
        const ImU32 col = colors[(&lang-langs.data())%colors.size()];
        for (const std::string& ext : G->languages.GetLanguage(lang)->extensions) {
            const std::string dot_ext = "."+ext;
            MscFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtention, dot_ext.c_str(), ImColor(col).Value);
        }
    }
    MscFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtention, ".png", ImColor(colors[langs.size()%colors.size()]).Value);
    MscFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByTypeDir, "", ImColor(0, 0, 0), "", GetFont(true, true, false));
}

bool escape_pressed = false;

void PasteClipBoardHelp() {
    if (std::holds_alternative<PptClipboard>(Config.clipboard_seen)) {
        const PptClipboard& seen = std::get<PptClipboard>(Config.clipboard_seen);
        if (seen.size()==1)
            Help(StrCat("There is a ", Config.FullChartType(seen.GetCharts().front().data.chart_type), " available to paste.").c_str());
        else if (seen.size()>1)
            Help(StrCat("There are ", std::to_string(seen.size()), " charts available to paste.").c_str());
    } else if (std::holds_alternative<EmbedChartData>(Config.clipboard_seen)) {
        const EmbedChartData& seen = std::get<EmbedChartData>(Config.clipboard_seen);
        if (seen.chart_type.size())
            Help(StrCat("There is a ", Config.FullChartType(seen.chart_type), " available to paste.").c_str());
    }
}

/** If do_welcome is set, we show the welcome screen.
 * If unset and use_lang is null, we show a small language selection dialog
 * and create an empty doc as the user selects.
 * If unset and use_lang is set, we create a new document of that type.
 * We ask if the user want to save any dirty docs.*/
StatusRet ConfigData::New(bool do_welcome, const LanguageData* use_lang) {
    enum Status {
        INITIAL,
        CHECK_OVERWRITE,
        SELECT_LANGUAGE,
        SELECT_FILE,
        LOAD,
        NEW,
        RECOVER,
        CLIPBOARD,
    };
    static Status status = INITIAL;
    static std::optional<RecentFile> load_this; //When status is LOAD, this is the file we attempt
    static std::optional<std::string> load_dir; //When status is LOAD, this is a dir we shall open the dialog in
    static const LanguageData* new_using_this_lang = nullptr;
    if (status == INITIAL) {
        //dont keep old selections hanging around, just because.
        load_this.reset();
        load_dir.reset();
        if (IsDirty()) status = CHECK_OVERWRITE;
        else status = SELECT_LANGUAGE;
    }
    if (status==CHECK_OVERWRITE) switch (CheckOverwrite("%s is changed. Do you want to save it?")) {
    case StatusRet::Ongoing: return StatusRet::Ongoing;
    case StatusRet::Cancelled: return StatusRet::Cancelled; //CheckOverwrite reports Cancelled if user selected save and cancelled
    case StatusRet::Completed: status = SELECT_LANGUAGE;
    }
    if (status==SELECT_LANGUAGE && !do_welcome && use_lang) {
        //skip language selection dialog if we are not on the welcome screen
        //and the user specified a language
        DestroyCharts();
        ResetText(use_lang);
        status = INITIAL;
        return StatusRet::Completed;
    }

    StatusRet ret = StatusRet::Ongoing;
    if (status==SELECT_LANGUAGE) {
        ImGui::OpenPopup(do_welcome ? "Welcome to Msc-generator" : "Select Language");
        // Always center this window when appearing
        ImVec2 center = ImGui::GetMainViewport()->GetCenter();
        ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
        if (ImGui::BeginPopupModal(do_welcome ? "Welcome to Msc-generator" : "Select Language", NULL,
                                   ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollbar
                                   | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoCollapse
                                   | ImGuiWindowFlags_NoSavedSettings)) {
            const ImVec2 button_size{150 * dpi_mul, 0};
            ImGui::SetItemDefaultFocus(); //Whatever button is first
            if (do_welcome) {
                ImGui::BeginTable("Welcome", 3, ImGuiTableFlags_SizingStretchProp
                                  | ImGuiTableFlags_BordersInnerV);
                ImGui::TableNextRow();
                ImGui::TableNextColumn();
                if ((!C->lang && recovered && recovered->source!=Source::Empty) || clipboard_seen.index()) {
                    ImGui::Text("Start from\n");
                    if ((!C->lang && recovered && recovered->source!=Source::Empty)) {
                        if (ImGui::Button("Recover last session", button_size)) {
                            ImGui::CloseCurrentPopup();
                            status = RECOVER;
                        }
                        switch (recovered->source) {
                        case Source::Clipboard:
                            Help("You were editing the content of the clipboard."); break;
                        case Source::File:
                        case Source::PNG:
                        case Source::PPT:
                            Help(StrCat("You were editing ", recovered->file.name).c_str()); break;
                        default:
                        case Source::Empty:
                            _ASSERT(0);
                            break;
                        }
                    }
                    if (clipboard_seen.index()) {
                        if (ImGui::Button("Clipboard", button_size)) {
                            ImGui::CloseCurrentPopup();
                            status = CLIPBOARD;
                        }
                        PasteClipBoardHelp();
                    }
                    ImGui::Spacing();
                    ImGui::Spacing();
                }
                ImGui::Text("Open Existing\n");
                if (ImGui::Button("Open", button_size)) {
                    ImGui::CloseCurrentPopup();
                    status = SELECT_FILE;
                }
                ImGui::Spacing();
                ImGui::Spacing();
                ImGui::Text("Create new\n");
            } else
                ImGui::Text("Available languages:\n");
            for (std::string_view lname : G->languages.GetLanguages())
                if (auto *l = G->languages.GetLanguage(lname);  ImGui::Button(l->description.c_str(), button_size)) {
                    ImGui::CloseCurrentPopup();
                    new_using_this_lang = l;
                    status = NEW;
                }
            ImGui::Spacing();
            ImGui::Spacing();
            if (ImGui::Button(C->lang ? "Cancel" : "Exit", button_size) || escape_pressed) {
                ImGui::CloseCurrentPopup();
                exit_requested = C->lang==nullptr; //User has cancelled initial language selection - we exit
                ret = StatusRet::Cancelled;
            }
            if (do_welcome) {
                ImGui::TableNextColumn();
                ImGui::Text("Open Recent\n");
                ImGui::Indent(10.f * dpi_mul);
                if (ImGui::BeginListBox("##Recent", {200 * dpi_mul, 250 * dpi_mul})) {
                    load_this = RecentFilesAsMenu();
                    if (load_this) {
                        status = LOAD;
                        ImGui::CloseCurrentPopup();
                    }
                    ImGui::EndListBox();
                }
                ImGui::TableNextColumn();
                ImGui::Text("Recent Directories\n");
                ImGui::Indent(10.f * dpi_mul);
                if (ImGui::BeginListBox("##RecentDir", {200 * dpi_mul, 250 * dpi_mul})) {
                    load_dir = RecentDirsAsMenu();
                    if (load_dir) {
                        status = LOAD;
                        ImGui::CloseCurrentPopup();
                    }
                    ImGui::EndListBox();
                }
                ImGui::EndTable();
            }
            ImGui::EndPopup();
        }
    }
    if (status == SELECT_FILE)
        ret = Load();
    if (status == NEW) {
        DestroyCharts();
        ResetText(new_using_this_lang);
        ret = StatusRet::Completed;
    }
    static std::string err_msg;
    if (status == RECOVER) {
        DestroyCharts();
        while (charts.size()<recovered->charts.size())
            charts.emplace_back().Init(*G->languages.cshParams);
        for (unsigned u = 0; EditorChartData &c : charts) {
            c.lang = G->languages.GetLanguage(recovered->charts[u].lang_str);
            c.slide = recovered->charts[u].slide;
            c.chart = recovered->charts[u].chart;
            c.editor.SetText(recovered->charts[u].text);
            c.editor.SetCursorPosition(recovered->charts[u].cursor_pos);
            c.saved_at = { -1, 0 };
            c.control_state = {};
            ++u;
        }
        file = recovered->file;
        source = recovered->source;
        if (recovered->zipped_clipboard_content.size())
            clipboard = PptClipboard(std::move(recovered->zipped_clipboard_content));
        //slides = std::move(recovered->slides);
        slides.clear();
        std::vector<PptFile::ChartInPPT> file_charts;
        err_msg = PptFile::parse_charts(ZipArchive::FromFile(recovered->file.path), file_charts, &slides);
        if (err_msg.empty()) {
            //TODO: merge changes in PPT
        }
        //Ensure we have as many slides as named in the charts
        if (slides.empty()) slides.resize(std::ranges::max_element(charts, {}, &EditorChartData::slide)->slide);
        recovered.reset();
        ret = StatusRet::Completed;
    }
    if (status == CLIPBOARD) {
        ret = Load(&clipboard_seen);
    }
    if (status == LOAD) {
        _ASSERT(load_this || load_dir);
        if (load_this) ret = Load(&*load_this);
        else if (load_dir) ret = Load(&*load_dir);
    }
    if (ret != StatusRet::Ongoing) status = INITIAL;

    return ret;
}

bool GUIError(const char *msg, bool append_errno = false) {
    bool proceed = false;
    ImGui::OpenPopup("Msc-generator##Error");
    // Always center this window when appearing
    ImVec2 center = ImGui::GetMainViewport()->GetCenter();
    ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
    if (ImGui::BeginPopupModal("Msc-generator##Error", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
        std::string storage;
        if (append_errno && errno) {
            storage = std::string(msg) + " (" + my_strerror(errno) + ")";
            msg = storage.c_str();
        }
        ImGui::Text("%s", msg);
        if (ImGui::Button("OK") || escape_pressed) {
            ImGui::CloseCurrentPopup();
            proceed = true;
        }
        ImGui::EndPopup();
    }
    return proceed;
}

StatusRet GUIQuestion(const char* msg) {
    ImGui::OpenPopup("Msc-generator##Overwrite");
    StatusRet ret = StatusRet::Ongoing;
    // Always center this window when appearing
    ImVec2 center = ImGui::GetMainViewport()->GetCenter();
    ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
    if (ImGui::BeginPopupModal("Msc-generator##Overwrite", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
        ImGui::TextUnformatted(msg);
        if (ImGui::Button("Yes") || ImGui::IsKeyPressed(ImGuiKey_Y)) {
            ImGui::CloseCurrentPopup();
            ret = StatusRet::Completed;
        }
        ImGui::SameLine();
        ImGui::SetItemDefaultFocus();
        if (ImGui::Button("No") || ImGui::IsKeyPressed(ImGuiKey_N) || escape_pressed) {
            ImGui::CloseCurrentPopup();
            ret = StatusRet::Cancelled;
        }
        ImGui::EndPopup();
    }
    return ret;
}

std::tuple<StatusRet, unsigned, std::string_view> GUIAddChart(const char* msg, const std::vector<std::string> &slides) {
    static int page_selected = 1;
    static int type_selected = 0;
    const static std::vector<std::string_view> languages = Config.G->languages.GetLanguages();
    ImGui::OpenPopup("Msc-generator##AddChart");
    StatusRet ret = StatusRet::Ongoing;
    // Always center this window when appearing
    ImVec2 center = ImGui::GetMainViewport()->GetCenter();
    ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
    ImGui::PushStyleColor(ImGuiCol_PopupBg, IM_COL32(240, 240, 240, 255));
    ImGui::PushStyleColor(ImGuiCol_FrameBg, IM_COL32(240, 240, 240, 255));
    if (ImGui::BeginPopupModal("Msc-generator##AddChart", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
        ImGui::TextUnformatted(msg);
        if (ImGui::BeginTable("##AddChartTable", 2, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_BordersInnerV)) {
            ImGui::TableNextRow();
            ImGui::TableNextColumn();
            ImGui::TextUnformatted("Slides");
            if (ImGui::BeginListBox("##Slides##AddChart", { Config.dpi_mul*250, 0 })) {
                ImGui::PushStyleColor(ImGuiCol_FrameBg, IM_COL32_WHITE);
                page_selected = std::min(page_selected, (int)slides.size());
                for (unsigned i = 1; i<=slides.size(); i++)
                    ImGui::RadioButton(("Page "+std::to_string(i)+(slides[i-1].empty() ? "" : ": "+slides[i-1])).c_str(), &page_selected, i);
                ImGui::RadioButton("Append new slide##0", &page_selected, 0);
                ImGui::PopStyleColor();
                ImGui::EndListBox();
            }
            ImGui::TableNextColumn();
            ImGui::TextUnformatted("Chart Types");
            if (ImGui::BeginListBox("##Chart Types##AddChart", { Config.dpi_mul*150, 0 })) {
                ImGui::PushStyleColor(ImGuiCol_FrameBg, IM_COL32_WHITE);
                for (auto& l : languages)
                    ImGui::RadioButton(Config.G->languages.GetLanguage(l)->description.c_str(), &type_selected, &l-&languages.front());
                ImGui::PopStyleColor();
                ImGui::EndListBox();
            }
            ImGui::EndTable();
        }
        if (ImGui::Button("Add Chart to PPT")) {
            ImGui::CloseCurrentPopup();
            ret = StatusRet::Completed;
        }
        ImGui::SameLine();
        ImGui::SetItemDefaultFocus();
        if (ImGui::Button("Cancel") || escape_pressed) {
            ImGui::CloseCurrentPopup();
            ret = StatusRet::Cancelled;
        }
        ImGui::EndPopup();
    }
    ImGui::PopStyleColor(2);
    return { ret, page_selected, languages[type_selected] };
}


//Returns
// - true if we need to add a chart, since we have no charts we recognized (string is add chart dialog message)
// - false if we could extract at least one chart (string is optional warning message)
std::pair<bool, std::string> ConfigData::LoadPpt(const std::vector<PptFile::ChartInPPT>& ppt_charts, const RecentFile& lfile) {
    std::pair<bool, std::string> ret{ false, {} };
    if (ppt_charts.empty())
        return ret = { false, "Could not find any charts." };
    std::string& errorText = ret.second;
    std::vector<unsigned> problem_charts;
    std::list<EditorChartData> new_charts;
    for (unsigned c = 0; c<ppt_charts.size(); c++)
        if (auto l = G->languages.GetLanguageByExtension(ppt_charts[c].data.chart_type)) {
            EditorChartData& CH = new_charts.emplace_back();
            CH.Init(*G->languages.cshParams);
            CH.lang = l;
            CH.editor.SetText(ppt_charts[c].data.chart_text);
            CH.control_state = ppt_charts[c].data.gui_state;
            CH.page = ppt_charts[c].data.page;
            CH.slide = ppt_charts[c].slide;
            CH.chart = ppt_charts[c].chart;
            CH.saved_at = CH.editor.GetUndoBufferIndex();
            CH.StartCompile();
        } else
            problem_charts.push_back(c);

    if (problem_charts.size()) {
        if (new_charts.empty()) ret = { true, "I found only charts I do not support: " };
        else errorText = "Some chart types I do not support: ";
        for (unsigned i : problem_charts) {
            errorText.append("'").append(ppt_charts[i].data.chart_type);
            if (ppt_charts[i].slide)
                errorText += StrCat("' on slide ", std::to_string(ppt_charts[i].slide), ":", std::to_string(ppt_charts[i].chart), ", ");
            else
                errorText += "', ";
        }
        errorText.pop_back();
        errorText.back() = '.';
    }
    if (!ret.first) {
        std::swap(new_charts, charts);
        C = &charts.front();
        file = lfile;
    }
    return ret;
}

/** Opens a file or a directory, or clipboard content (in the latter case we move out from PptClipboard.
 * If any of the pointers are NULL, we display the file dialog.*/
StatusRet ConfigData::Load(std::variant<const RecentFile*, const std::string*, ClipboardSeen *> load_this) {
    enum Status {
        INITIAL,
        SELECT,
        CHECK_OVERWRITE,
        LOAD,
        ADD_CHART,
        SHOW_ERROR_SUCCESS,
        SHOW_ERROR_CANCEL,
        CANCELLED,
    };
    static Status status = INITIAL;
    static std::string errorText;
    static bool errorIncludeErrno = false;
    static std::string add_chart_msg;
    static RecentFile lfile;
    static std::string dir_to_open = ".";
    if (status == INITIAL) {
        errorText.clear();
        errorIncludeErrno = false;
        if (std::holds_alternative<const RecentFile*>(load_this) && std::get<const RecentFile*>(load_this)) {
            //open a specific file without displaying a dialog
            lfile = *std::get<const RecentFile*>(load_this);
            status = CHECK_OVERWRITE;
        } else if (std::holds_alternative<const std::string*>(load_this) && std::get<const std::string*>(load_this)) {
            //open a the dialog in a specific directory
            lfile.clear();
            _ASSERT(std::get<const std::string*>(load_this)->size());
            if (std::get<const std::string*>(load_this)->size())
                dir_to_open = *std::get<const std::string*>(load_this);
            status = SELECT;
        } else if (std::holds_alternative<ClipboardSeen*>(load_this) && std::get<ClipboardSeen*>(load_this)) {
            status = CHECK_OVERWRITE;
        } else {
            if (dir_to_open=="." && file.path.size())
                dir_to_open = DirNameFromPath(file.path);
            lfile.clear();
            status = SELECT;
        }
        if (status==SELECT)
            MscFileDialog::Instance()->DeserializeBookmarks(GetSerializedRecentDirs().c_str());
    }
    if (status == SELECT) {
        MscFileDialog::Instance()->OpenDialog("FileDialog##Open", "Open file", open_filter_groups.c_str(), dir_to_open, "");
        ImGui::SetNextWindowSize(ImVec2{800, 400}*Config.dpi_mul, ImGuiCond_FirstUseEver);
        ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Once, ImVec2(0.5f, 0.5f));
        if (MscFileDialog::Instance()->Display("FileDialog##Open", ImGuiWindowFlags_NoCollapse,
                                                 ImVec2{ 200,100 }*Config.dpi_mul, ImGui::GetMainViewport()->WorkSize * 0.9f)) {
            if (MscFileDialog::Instance()->IsOk()) {
                lfile.path = MscFileDialog::Instance()->GetFilePathName();
                lfile.name = MscFileDialog::Instance()->GetCurrentFileName();
                status = CHECK_OVERWRITE;
            } else
                status = INITIAL;
            MscFileDialog::Instance()->Close();
            UpdateRecentDirs(MscFileDialog::Instance()->SerializeBookmarks());
        }
        if (escape_pressed) {
            status = INITIAL;
            MscFileDialog::Instance()->Close();
            UpdateRecentDirs(MscFileDialog::Instance()->SerializeBookmarks());
        }
    }
    if (status == CHECK_OVERWRITE) switch (CheckOverwrite("%s is changed. Do you want to save it?")) {
    case StatusRet::Ongoing: break;
    case StatusRet::Cancelled: status = CANCELLED; break;
    case StatusRet::Completed: status = LOAD; break;
    }
    static std::vector<std::string> new_slides;
    if (status == LOAD) {
        if (std::holds_alternative<ClipboardSeen*>(load_this)) {
            if (std::get<ClipboardSeen*>(load_this)) {
                ClipboardSeen& seen = *std::get<ClipboardSeen*>(load_this);
                if (std::holds_alternative<PptClipboard>(seen)) {
                    bool need_to_add;
                    std::tie(need_to_add, errorText) = LoadPpt(std::get<PptClipboard>(seen).GetCharts(), RecentFile{});
                    if (!need_to_add) {
                        clipboard = std::move(std::get<PptClipboard>(seen));
                        source = Source::Clipboard;
                    }
                    status = errorText.empty() ? INITIAL : need_to_add ? SHOW_ERROR_CANCEL : SHOW_ERROR_SUCCESS;
                } else if (std::holds_alternative<EmbedChartData>(seen)) {
                    EmbedChartData& data = std::get<EmbedChartData>(seen);
                    if (auto l = G->languages.GetLanguageByExtension(data.chart_type)) {
                        //Load into the current chart and delete the rest
                        DestroyCharts();
                        ResetText(l, &data.chart_text);
                        status = INITIAL;
                    } else {
                        errorText = "Extension '"+data.chart_type+"' is not a language I support.";
                        status = SHOW_ERROR_CANCEL;
                    }
                } else
                    status = INITIAL; //seen contains nothing we understand
                seen = std::monostate{}; //reset clipboard_seen, so that we hide the 'paste clipboard' button
            } else
                status = INITIAL; //load_this contains nullptr
        } else if (const size_t dot_pos = lfile.path.find_last_of('.'); dot_pos == lfile.path.npos) {
            DelRecentFile(lfile);
            errorText = "Could not recognize file type.";
            status = SHOW_ERROR_CANCEL;
        } else if (std::string type(lfile.path.substr(dot_pos+1))
                   ; type=="pptx") {
            std::vector<PptFile::ChartInPPT> ppt_charts;
            new_slides.clear();
            errorText = PptFile::parse_charts(ZipArchive::FromFile(lfile.path), ppt_charts, &new_slides);
            if (errorText.empty()) {
                if (ppt_charts.empty()) {
                    status = ADD_CHART;
                    add_chart_msg = " contains no charts embedded by Msc-generator.";
                    return StatusRet::Ongoing; //Will come back and go to ADD_CHART below
                }
                bool need_to_add;
                std::tie(need_to_add, errorText) = LoadPpt(ppt_charts, lfile);
                if (need_to_add) { //No recognized chart type
                    add_chart_msg = std::move(errorText);
                    errorText.clear();
                    status = ADD_CHART;
                    return StatusRet::Ongoing; //Will come back and go to ADD_CHART below
                }
                slides = std::move(new_slides);
                source = Source::PPT;
                AddRecentFile(std::move(lfile)); //leave lfile in undef state we will end up INITIAL anyway
                status = errorText.empty() ? INITIAL : need_to_add ? SHOW_ERROR_CANCEL: SHOW_ERROR_SUCCESS;
            } else
                status = SHOW_ERROR_CANCEL;
        } else {
            std::string text, gui_state;
            unsigned page = 0;
            Source src = Source::Empty;
            if (type=="png") {
                if (FILE* in = G->file_open_proc(lfile.path.c_str(), false, true)) {
                    MscError Error;
                    EmbedChartData chart = pngutil::Select(in, G->languages, Error);
                    fclose(in);
                    if (Error.hasErrors()) {
                        DelRecentFile(lfile);
                        errorText = Error.GetError(0, false, false)->message;
                    } else {
                        _ASSERT(!chart.empty());
                        type = std::move(chart.chart_type);
                        text = std::move(chart.chart_text);
                        gui_state = std::move(chart.gui_state);
                        page = chart.page;
                        src = Source::PNG;
                    }
                } else {
                    DelRecentFile(lfile);
                    errorText = "Could not open '"+lfile.path+"'. ";
                    errorIncludeErrno = true;
                }
            } else if (G->languages.GetLanguageByExtension(type) == nullptr) {
                DelRecentFile(lfile);
                errorText = "Extension '"+type+"' is not a language I support.";
            } else if (FILE *in = G->file_open_proc(lfile.path.c_str(), false, false)) {
                text = ReadFile(in);
                fclose(in);
                src = Source::File;
            } else {
                DelRecentFile(lfile);
                errorText = "Could not open '"+lfile.path+"'. ";
                errorIncludeErrno = true;
            }
            if (errorText.empty()) {
                _ASSERT(src!=Source::Empty);
                if (auto l = G->languages.GetLanguageByExtension(type)) { //re-lookup as for PNG we did not
                    DestroyCharts();
                    //Load into the current chart and delete the rest
                    C->lang = l;
                    C->editor.SetText(text);
                    C->control_state = std::move(gui_state);
                    C->page = page;
                    C->saved_at = C->editor.GetUndoBufferIndex();
                    C->chart = C->slide = 0;
                    file = lfile;
                    source = src;
                    AddRecentFile(std::move(lfile)); //leave lfile in undef state we will end up INITIAL anyway
                    C->StartCompile();
                } else {
                    DelRecentFile(lfile);
                    errorText = "Extension '"+type+"' is not a language I support.";
                }
            }
            status = errorText.empty() ? INITIAL : SHOW_ERROR_CANCEL;
        }
    }
    if (status == ADD_CHART) {
        auto [res, slide, lang_str] = GUIAddChart((lfile.name+add_chart_msg+" Select one to add.").c_str(), new_slides);
        switch (res) {
        case StatusRet::Ongoing: break;
        case StatusRet::Cancelled: status = CANCELLED; break;
        case StatusRet::Completed:
            DestroyCharts();
            //Reset into the current chart and delete the rest
            ResetText(G->languages.GetLanguageByExtension(lang_str));
            C->saved_at = { -1, 0 }; //start as dirty
            C->slide = slide;
            C->chart = 0;
            C->page = 0;
            C->StartCompile();
            file = lfile;
            source = Source::PPT;
            AddRecentFile(std::move(lfile)); //leave lfile in undef state we will end up INITIAL anyway
            status = INITIAL;
        }
    }
    if (status == SHOW_ERROR_SUCCESS || status == SHOW_ERROR_CANCEL)
        if (GUIError(errorText.c_str(), errorIncludeErrno))
            status = status == SHOW_ERROR_SUCCESS ? INITIAL : CANCELLED;
    switch (status) {
    case INITIAL: return StatusRet::Completed;
    case CANCELLED: status = INITIAL;  return StatusRet::Cancelled;
    default: return StatusRet::Ongoing;
    }
}

//We return Cancelled if the user pressed Cancel or selected Save and cancelled the selection of the filename.
StatusRet ConfigData::CheckOverwrite(const char *message) {
    static bool ask = true; //this is our status
    if (!IsDirty()) {
        ask = true;
        return StatusRet::Completed;
    }
    StatusRet ret = StatusRet::Ongoing;
    if (ask) {
        ImGui::OpenPopup("Msc-generator");
        // Always center this window when appearing
        ImVec2 center = ImGui::GetMainViewport()->GetCenter();
        ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
        if (ImGui::BeginPopupModal("Msc-generator", NULL, ImGuiWindowFlags_AlwaysAutoResize)) {
            ImGui::Text(message, source==Source::Clipboard ? "The chart copied from the clipboard" : file.name.c_str());
            if (ImGui::Button("Yes") || ImGui::IsKeyPressed(ImGuiKey_Y)) {
                ImGui::CloseCurrentPopup();
                ask = false;
            }
            ImGui::SameLine();
            ImGui::SetItemDefaultFocus();
            if (ImGui::Button("No") || ImGui::IsKeyPressed(ImGuiKey_N)) {
                ImGui::CloseCurrentPopup();
                ret = StatusRet::Completed;
            }
            ImGui::SameLine();
            if (ImGui::Button("Cancel") || escape_pressed) {
                ImGui::CloseCurrentPopup();
                ret = StatusRet::Cancelled;
            }
            ImGui::EndPopup();
        }
    } else
        ret = Save(SaveMode::Interactive);
    if (ret != StatusRet::Ongoing) ask = true;
    return ret;
}

//If mode==Quiet, we will not return Ongoing, but Completed on success and Cancelled on failure.
StatusRet ConfigData::Save(SaveMode mode) {
    if (!C->lang) return StatusRet::Completed;
    static std::string _full_path, _file_name, _ext; //names selected by the user - or the old ones (then _ext is empty)
    enum Status {
        INITIAL,          //This function has never been called or the previous workflow has ended
        SELECT,           //The user is currently selecting a filename, the file dialog is open
        CHECK_OVERWRITE,  //The user has selected a file, we check if we want to overwrite an existing file
        ASK_OVERWRITE,    //The user has selected a file, which already exists
        RECOMPILE_CHECK_EXT,//Recompile for PNG and PPT - for other fie types, check if a compatible extension is selected
        SHOW_ERROR,        //The save had an error and we are waiting for the user to ack it.
        CANCELLED,
        COPY_CLIPBOARD     //We are copying to the clipboard
    };
    static Status status = INITIAL;
    static std::string err_msg;

    //True if we have opened something from the clipboard and save it back there
    const bool update_to_clipboard = (mode==SaveMode::Quiet || mode==SaveMode::Interactive) && source==Source::Clipboard;
    if (status==INITIAL) {
        if (update_to_clipboard || mode==SaveMode::CopyToClipboard) {
            _full_path.clear();
            _file_name.clear();
            _ext.clear();
            status = RECOMPILE_CHECK_EXT;
        } else if (mode==SaveMode::As || (mode==SaveMode::Interactive && source==Source::File && file.path.empty())) {
            //open dialog, we save to a file
            _full_path.clear();
            _file_name.clear();
            _ext.clear();
            status = SELECT;
            MscFileDialog::Instance()->DeserializeBookmarks(GetSerializedRecentDirs().c_str());
        } else if (mode==SaveMode::Interactive || file.path.size()) {
            //Re-use existing file name
            _ASSERT(source==Source::File || source==Source::PNG || source==Source::PPT);
            _full_path = file.path;
            _file_name = file.name;
            const size_t dot_pos = _file_name.find_last_of('.');
            _ext = dot_pos!=_file_name.npos
                ? _file_name.substr(dot_pos+1) //extension of the current filename
                : C->lang->extensions.front(); //or the canonical extension
            status = RECOMPILE_CHECK_EXT; //go to save directly
        } //else if quiet and no filename and no clipboard, keep INITIAL status - do nothing & return Completed
    }
    if (status==SELECT) {
        std::string filter = C->lang->description + " (";
        for (const std::string &e : C->lang->extensions)
            filter.append("*.").append(e).push_back(' ');
        filter.back() = ')';
        filter.append("{");
            for (const std::string &e : C->lang->extensions)
                filter.append(".").append(e).push_back(',');
        filter.back() = '}';
        std::string filter2 = "Embedded chart (*.png){.png}";
        if (file.name.ends_with(".png")) std::swap(filter, filter2); //'Save as' on a png shall offer PNG as primary extension
        filter.append(",").append(filter2);
        std::string dialog_dir(DirNameFromPath(file.path));
        if (dialog_dir.empty()) dialog_dir = last_dir;
        MscFileDialog::Instance()->OpenDialog("FileDialog##Save", SaveMode::As == mode ? "Save file as" : "Save file", filter.c_str(), dialog_dir, file.name);
        ImGui::SetNextWindowSize(ImVec2{800, 400}*Config.dpi_mul, ImGuiCond_FirstUseEver);
        ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Once, ImVec2(0.5f, 0.5f));
        if (MscFileDialog::Instance()->Display("FileDialog##Save", ImGuiWindowFlags_NoCollapse,
                                                 ImVec2{ 200,100 }*Config.dpi_mul, ImGui::GetMainViewport()->WorkSize * 0.9f)) {
            if (MscFileDialog::Instance()->IsOk()) {
                _full_path = MscFileDialog::Instance()->GetFilePathName();
                _file_name = MscFileDialog::Instance()->GetCurrentFileName();
                const bool filter_is_png = MscFileDialog::Instance()->GetCurrentFilter().find("*.png")!=std::string::npos;
                const size_t dot_pos = _file_name.find_last_of('.');
                _ext = dot_pos!=_file_name.npos
                    ? _file_name.substr(dot_pos+1)                         //user supplied extension
                    : filter_is_png ? "png" : C->lang->extensions.front(); //png or the canonical extension
                if (dot_pos==_file_name.npos) {
                    _file_name.append(".").append(_ext);
                    _full_path.append(".").append(_ext);
                }
                status = CHECK_OVERWRITE;
            } else
                status = CANCELLED;
            MscFileDialog::Instance()->Close();
            UpdateRecentDirs(MscFileDialog::Instance()->SerializeBookmarks());
        }
        if (escape_pressed) {
            status = INITIAL;
            MscFileDialog::Instance()->Close();
            UpdateRecentDirs(MscFileDialog::Instance()->SerializeBookmarks());
        }
    }
    if (status == CHECK_OVERWRITE) {
        //Here we save to a file
        if (_full_path==file.path)
            status = RECOMPILE_CHECK_EXT;
        else if (std::ifstream(_full_path).good())
            status = ASK_OVERWRITE;
        else
            status = RECOMPILE_CHECK_EXT;
    }
    if (status == ASK_OVERWRITE)
        switch (GUIQuestion(("File '"+_file_name+"' already exists. Do you want to overwrite it?").c_str())) {
            case StatusRet::Ongoing: break;
            case StatusRet::Cancelled: status = SELECT; break;
            case StatusRet::Completed: status = RECOMPILE_CHECK_EXT; break;

        }
    static std::string zipped_clipboard;
    static std::string type_clipboard;
    if (status==RECOMPILE_CHECK_EXT) {
        std::string msg;
        //First recompile pptx and png files, if not in quiet mode & check filename extension for other types
        if ((_ext=="pptx" || update_to_clipboard) && mode!=SaveMode::Quiet) { //do not recompile on quiet save
            if (std::ranges::all_of(charts, [](EditorChartData& c) { return c.CompileIfNeeded(); })) {
                const size_t err = std::ranges::count_if(charts, [](EditorChartData& c) { return c.pChart && c.pChart->Error.hasErrors(); });
                if (!err)
                    status = SHOW_ERROR;        //OK, next phase is actual saving
                else {
                    if (charts.size()==1)
                        msg = "The chart had errors, do you want to save it into the PPT nevertheless?";
                    else if (err==1)
                        msg = "One of the chart had errors, do you want to save the charts into the PPT nevertheless?";
                    else if (err==charts.size())
                        msg = "All of the charts had errors, do you want to save them into the PPT nevertheless?";
                    else
                        msg = "Some of the charts had errors, do you want to save all of the charts into the PPT nevertheless?";
                    if (update_to_clipboard) {
                        constexpr std::string_view into_the_PPT = "into the PPT";
                        constexpr std::string_view to_the_clipboard = "to the clipboard for insertion into Office";
                        msg.replace(msg.find(into_the_PPT), into_the_PPT.size(), to_the_clipboard) ;
                    }
                    goto display_warning;
                }
            }
        } else if (mode==SaveMode::CopyToClipboard) {
            if (C->CompileIfNeeded()) { //result will be taken in the main loop
                if (C->pChart && !C->pChart->Error.hasErrors())
                    status = SHOW_ERROR; //OK, next phase is actual saving
                else {
                    msg = "The chart had errors, do you want to copy it to clipboard for insertion into Office?";
                    goto display_warning;
                }
            } else
                status = SHOW_ERROR; //Compiled, next phase is actual saving
        } else if (_ext=="png" && mode!=SaveMode::Quiet) { //do not recompile on quiet save
            if (C->CompileIfNeeded()) { //result will be taken in the main loop
                if (C->pChart && !C->pChart->Error.hasErrors())
                    status = SHOW_ERROR; //OK, next phase is actual saving
                else {
                    msg = "The chart had errors, do you want to save it into a PNG nevertheless?";
                display_warning:
                    ImGui::OpenPopup("Warning");
                    // Always center this window when appearing
                    ImVec2 center = ImGui::GetMainViewport()->GetCenter();
                    ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
                    if (ImGui::BeginPopupModal("Warning")) {
                        ImGui::TextUnformatted(msg.c_str());
                        ImGui::SetItemDefaultFocus();
                        if (ImGui::Button("Save")) {
                            ImGui::CloseCurrentPopup();
                            status = SHOW_ERROR;
                        }
                        ImGui::SameLine();
                        if (ImGui::Button("Cancel") || escape_pressed) {
                            ImGui::CloseCurrentPopup();
                            status = CANCELLED;
                        }
                        ImGui::EndPopup();
                    }
                }
            }
        } else {
            //We get here for PPT and PNG files in Quiet mode and for all other chart types
            if (std::ranges::find(C->lang->extensions, _ext)==C->lang->extensions.end()
                    && !CaseInsensitiveEqual(_ext, "png") && !CaseInsensitiveEqual(_ext, "pptx")) {
                //we do not recognize the input file type
                if (mode == SaveMode::Quiet)
                    status = CANCELLED;
                else {
                    ImGui::OpenPopup("Warning");
                    // Always center this window when appearing
                    ImVec2 center = ImGui::GetMainViewport()->GetCenter();
                    ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
                    if (ImGui::BeginPopupModal("Warning")) {
                        ImGui::Text("Changing the file extension to one not supported by this language.");
                        ImGui::Text("Consider using one of %s.", PrintStrings(C->lang->extensions).c_str());
                        ImGui::SetItemDefaultFocus();
                        if (ImGui::Button(("Use "+_ext).c_str())) {
                            ImGui::CloseCurrentPopup();
                            status = SHOW_ERROR;
                        }
                        ImGui::SameLine();
                        if (ImGui::Button("Cancel") || escape_pressed) {
                            ImGui::CloseCurrentPopup();
                            status = CANCELLED;
                        }
                        ImGui::EndPopup();
                    }
                }
            } else
                status = SHOW_ERROR;
        }
        err_msg.clear();
        if (status == SHOW_ERROR) {
            //Do the actual saving. For PPT and PNG use the last compiled pChart (if any)
            if (mode==SaveMode::CopyToClipboard) {
                zipped_clipboard = {};
                if (C->pChart) { //maybe false on quiet save
                    std::string tmp_filename = my_tmpnam();
                    if (C->pChart->DrawToFile(Canvas::PNG, XY(4, 4), tmp_filename, true, true, nullptr, C->page)) {
                        EmbedChartData data{ .chart_type = C->lang->GetName(), .chart_text = C->editor.GetText(),
                                             .chart_size = C->pChart->GetCanvasSize(C->page), .gui_state = C->pChart->SerializeGUIState(),
                                             .page = C->page };
                        if (FILE* png = G->file_open_proc(tmp_filename.c_str(), false, true)) {
                            zipped_clipboard = PptClipboard::create(std::move(data), ReadFile(png));
                            fclose(png);
                        }
                        std::error_code ec;
                        std::filesystem::remove(tmp_filename, ec);
                    } //else silently fail below
                }//else silently fail below
                if (zipped_clipboard.size()) {
                    type_clipboard = C->lang->description;
                    status = COPY_CLIPBOARD;
                } else
                    status = INITIAL; //silently fail.
            } else if (_ext=="pptx" || update_to_clipboard) {
                std::vector<PptFile::ChartToPPT> tmp_files;
                std::vector<EditorChartData*> tmp_charts; //which Chart went into the corresponding index in tmp_files.
                for (EditorChartData& c : charts)
                    if (!c.pChart) continue;   //pChart maybe empty on quiet save.
                    else {
                        const bool changed = file.path != _full_path  //the filename changed, save every chart
                            || c.saved_at != c.compiled_at            //the chart text changed, save it
                            || (update_to_clipboard && [&]() {
                                auto* pC = clipboard.FindChart(c.slide, c.chart);
                                if (!pC) return true;                 //A new addition to the clipboard, save
                                return c.page != pC->data.page        //The page has changed, save it
                                    || c.pChart->SerializeGUIState() != pC->data.gui_state;  //The gui state has changed, save it
                            }());
                        if (changed) { // Skip saving if not changed
                            tmp_charts.push_back(&c);
                            EmbedChartData data{.chart_type = c.lang->GetName(), .chart_text = c.editor.GetText(),
                                                    .chart_size = c.pChart->GetCanvasSize(c.page), .gui_state = c.pChart->SerializeGUIState(),
                                                    .page = c.page};
                            tmp_files.emplace_back(std::move(data), c.slide, c.chart, my_tmpnam());
                            if (!c.pChart->DrawToFile(Canvas::PNG, XY(4, 4), tmp_files.back().png_fn, true, true, nullptr, c.page)) {
                                err_msg = "Could not write to temp PNG file: '" + tmp_files.back().png_fn + "'.";
                                goto delete_tmp_files;
                            }
                        }
                    }
                if (update_to_clipboard) {
                    err_msg = clipboard.update(tmp_files, G->file_open_proc);
                    if (err_msg.empty()) {
                        if (clipboard.size()==1) type_clipboard = charts.front().lang->description;
                        else type_clipboard = "Multiple charts";
                        zipped_clipboard = clipboard.GetZippedContent();
                        status = COPY_CLIPBOARD;
                    }
                } else
                    err_msg = PptFile::add_images(ZipArchive::FromFile(_full_path), tmp_files, G->file_open_proc);
            delete_tmp_files:
                std::error_code ec;
                for (auto& f : tmp_files)
                    std::filesystem::remove(f.png_fn, ec);
                if (err_msg.empty()) {
                    for (unsigned u = 0; u<tmp_files.size(); u++) {
                        tmp_charts[u]->saved_at = tmp_charts[u]->compiled_at;
                        tmp_charts[u]->slide = tmp_files[u].slide;
                        tmp_charts[u]->chart = tmp_files[u].chart;
                        if (slides.size()<tmp_files[u].slide)
                            slides.resize(tmp_files[u].slide);
                    }
                    if (status != COPY_CLIPBOARD) {
                        file.path = _full_path;
                        file.name = _file_name;
                        AddRecentFile(RecentFile{ std::move(_full_path), std::move(_file_name) });
                        if (mode!=SaveMode::Quiet)
                            AddMessage({ "Saved successfully.", 5 });
                        status = INITIAL;
                    }
                }
            } else {
                //We save even if no change since last save
                bool write_ok = false;
                if (_ext=="png") {
                    if (C->pChart) //maybe false on quiet save
                        write_ok = C->pChart->DrawToFile(Canvas::PNG, XY(1, 1), _full_path, true, true, C->editor.GetText().c_str());
                } else if (FILE* out = G->file_open_proc(_full_path.c_str(), true, false)) {
                    std::string text = C->editor.GetText();
                    write_ok = text.size() == fwrite(text.data(), 1, text.size(), out);
                    fclose(out);
                }
                if (write_ok) {
                    file.path = std::move(_full_path);
                    file.name = std::move(_file_name);
                    C->saved_at = C->editor.GetUndoBufferIndex();
                    AddRecentFile(RecentFile{ file });
                    if (mode!=SaveMode::Quiet)
                        AddMessage({ "Saved successfully.", 5 });
                    source = _ext=="png" ? Source::PNG : Source::File;
                    status = INITIAL;
                } else
                    err_msg = "Error writing file.";
                    //fallthrough to the SHOW_ERROR test below - and go there directly from the next frame until user ACKs the GUIError()
            }
        }
    }
    if (status==COPY_CLIPBOARD) //We may need to re-try several times, so it is a state of its own
        if (CopyToClipboard(ClipboardFormat::Art_GVML, zipped_clipboard, type_clipboard)) {
            AddMessage({ "Copied to clipboard.", 5 }, true);
            status = INITIAL;
        }

    if (status==SHOW_ERROR)
        if (mode==SaveMode::Quiet || GUIError(err_msg.c_str(), true)) //do not show error on quiet
            status = INITIAL;
    switch (status) {
    case INITIAL: return StatusRet::Completed; //if we are set back to initial, we are done.
    case CANCELLED: status = INITIAL; return StatusRet::Cancelled;
    default: return StatusRet::Ongoing;
    }
}


StatusRet ConfigData::Export(bool reuse_last_name) {
    enum Status {
        INITIAL,          //This function has never been called or the previous workflow has ended
        SELECT,           //The user is currently selecting a filename, the file dialog is open
        CHECK_OVERWRITE,  //The user has selected a file, we check if we want to overwrite an existing file
        ASK_OVERWRITE,    //The user has selected a file, which already exists
        CONFIRM_PROBLEM,  //The user has selected a file and we save - or a problematic file and we wait for confirming the choice
        SET_OPTIONS,      //The user is setting the options for the file
        SHOW_ERROR,       //The save had an error and we are waiting for the user to ack it.
        CANCELLED
    };
    static Status status = INITIAL;
    static std::string _full_path, _file_name; //names selected by the user (only valid in CONFIRM_PROBLEM or SET_OPTIONS - also kept for the next invocation)
    static Canvas::EOutputType _type;          //output type selected by user (only valid in CONFIRM_PROBLEM or SET_OPTIONS - also kept for the next invocation)

    struct Filter { Canvas::EOutputType type;  std::string_view text, ext1, ext2 = {}; };
    static std::vector<Filter> filters = {
        {Canvas::PNG, "Portable Network Graphics", "png"},
        {Canvas::SVG, "Scalable Vector Graphics", "svg"},
        {Canvas::PDF, "Portable Document Format", "pdf"},
        {Canvas::PPT, "PowerPoint Document", "pptx", "ppt"},
        {Canvas::EPS, "Encapsulated PostScript", "eps"},
#ifdef _WIN32
        {Canvas::EMF, "Enhanced Metafile", "emf"},
        {Canvas::WMF, "Windows Metafile", "wmf"},
#endif
    };
    static std::string filter_str;
    if (status==INITIAL) {
        reuse_last_name &= bool(_full_path.size());
        if (reuse_last_name) {
            status = CONFIRM_PROBLEM;
        } else {
            _file_name = file.name;
            if (size_t pos = _file_name.find_last_of('.'); pos!=std::string::npos)
                _file_name.resize(pos); //remove extension
            if (Config.export_format.size())
                std::ranges::partition(filters, [](const Filter& f) {return f.ext1==Config.export_format; }); //have last format first
            filter_str.clear();
            for (const Filter& f: filters) {
                filter_str.append(f.text).append(" (*.").append(f.ext1);
                if (f.ext2.size()) filter_str.append(" *.").append(f.ext2);
                filter_str.append("){.").append(f.ext1);
                if (f.ext2.size()) filter_str.append(",.").append(f.ext2);
                filter_str.append("}");
            }
            status = SELECT;
            MscFileDialog::Instance()->DeserializeBookmarks(GetSerializedRecentDirs().c_str());
        }
    }
    if (status==SELECT) {
        MscFileDialog::Instance()->OpenDialog("FileDialog##Export", "Export", filter_str.c_str(), Config.export_dir.empty() ? file.path : Config.export_dir, _file_name);
        ImGui::SetNextWindowSize(ImVec2{800, 400}*Config.dpi_mul, ImGuiCond_FirstUseEver);
        ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Once, ImVec2(0.5f, 0.5f));
        if (MscFileDialog::Instance()->Display("FileDialog##Export", ImGuiWindowFlags_NoCollapse,
                                                 ImVec2{ 200,100 }*Config.dpi_mul, ImGui::GetMainViewport()->WorkSize * 0.9f)) {
            if (MscFileDialog::Instance()->IsOk()) {
                _full_path = MscFileDialog::Instance()->GetFilePathName();
                _file_name = MscFileDialog::Instance()->GetCurrentFileName();
                const std::string selected_filter = MscFileDialog::Instance()->GetCurrentFilter();
                const auto i = std::ranges::find_if(filters, [&selected_filter](const Filter& f) {return selected_filter.starts_with(f.text); });
                if (i==filters.end()) {
                    _ASSERT(0);
                    status = INITIAL;
                } else {
                    if (const std::string ext = "."+std::string(i->ext1); !_file_name.ends_with(ext)) {
                        _file_name.append(ext);
                        _full_path.append(ext);
                    }
                    _type = i->type;
                    Config.export_format = i->ext1;
                    Config.export_dir = DirNameFromPath(_full_path);
                    Config.last_export_file = RecentFile{ _full_path, _file_name };
                    status = CHECK_OVERWRITE;
                }
            } else
                status = CANCELLED;
            MscFileDialog::Instance()->Close();
            UpdateRecentDirs(MscFileDialog::Instance()->SerializeBookmarks());
        }
        if (escape_pressed) {
            status = INITIAL;
            MscFileDialog::Instance()->Close();
            UpdateRecentDirs(MscFileDialog::Instance()->SerializeBookmarks());
        }
    }
    if (status == CHECK_OVERWRITE) {
        if (_full_path==file.path) //exporting a png to the same name is OK
            status = CONFIRM_PROBLEM;
        else if (std::ifstream(_full_path).good())
            status = ASK_OVERWRITE;
        else
            status = CONFIRM_PROBLEM;
    }
    if (status == ASK_OVERWRITE)
        switch (GUIQuestion(("File '"+_file_name+"' already exists. Do you want to overwrite it?").c_str())) {
        case StatusRet::Ongoing: break;
        case StatusRet::Cancelled: status = SELECT; break;
        case StatusRet::Completed: status = CONFIRM_PROBLEM; break;

        }

    static int x, y;
    if (status==CONFIRM_PROBLEM) {
        if (C->CompileIfNeeded()) {
            x = int(C->pChart->GetTotal().x.Spans());
            y = int(C->pChart->GetTotal().y.Spans());
            if (C->pChart->Error.hasErrors()) {
                ImGui::OpenPopup("Warning");
                // Always center this window when appearing
                ImVec2 center = ImGui::GetMainViewport()->GetCenter();
                ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
                if (ImGui::BeginPopupModal("Warning")) {
                    ImGui::Text("The chart had errors, do you want to export it nevertheless ?");
                    ImGui::SetItemDefaultFocus();
                    if (ImGui::Button("Export")) {
                        ImGui::CloseCurrentPopup();
                        status = SET_OPTIONS;
                    }
                    ImGui::SameLine();
                    if (ImGui::Button("Cancel") || escape_pressed) {
                        ImGui::CloseCurrentPopup();
                        status = CANCELLED;
                    }
                    ImGui::EndPopup();
                }
            } else
                status = SET_OPTIONS;
        }
    }
    static std::string error;
    XY sc(1, 1); //scale
    bool ipbrk = true; //ignore page breaks
    //the below are for for "-p" type exports
    XY ps(0, 0); //page_size
    int ha = 0, va = 0;
    const double* pMargins = nullptr;
    std::array<float, 4> ppt_gui_margins = { 0,0,0,0 };
    static const std::array page_sizes = []{
        std::array<const char*, unsigned(EPageSize::MAX_PAGE)-unsigned(EPageSize::NO_PAGE)-1> ret;
        for (unsigned u = 0; u<ret.size(); u++)
            ret[u] = ConvertPageSizeVerbose(EPageSize(u+unsigned(EPageSize::NO_PAGE)+1)).data(); //We assume null-terminated string-view
        return ret;
    }();
    static int ignore_pagebreaks;
    if (status == SET_OPTIONS) {
        if (reuse_last_name) status = SHOW_ERROR; //skip adjustment dialog
        else switch (_type) {
        case Canvas::PNG:
            ImGui::OpenPopup("PNG Options");
            ImGui::PushStyleColor(ImGuiCol_PopupBg, IM_COL32(240, 240, 240, 255));
            if (ImGui::BeginPopupModal("PNG Options", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoResize)) {
                ImGui::Text("Please select how to scale the resulting bitmap.");
                ImGui::RadioButton(("No scaling ("+std::to_string(x)+" x "+std::to_string(y)+")").c_str(), &export_bitmap_scale_selection, 0);
                ImGui::RadioButton("Scale by a factor of", &export_bitmap_scale_selection, 1);
                  ImGui::BeginDisabled(export_bitmap_scale_selection!=1);
                  ImGui::SameLine();
                  ImGui::SetNextItemWidth(30*Config.dpi_mul);
                  ImGui::InputDouble("##scale", &export_bitmap_scale_factor, 0.f, 0.f, "%g");
                  ImGui::EndDisabled();
                ImGui::RadioButton("Keep aspect ratio and set width to", &export_bitmap_scale_selection, 2);
                  ImGui::BeginDisabled(export_bitmap_scale_selection!=2);
                  ImGui::SameLine();
                  ImGui::SetNextItemWidth(30*Config.dpi_mul);
                  ImGui::InputInt("##xsize", &x, 0, 0);
                  ImGui::EndDisabled();
                  ImGui::SameLine();
                  ImGui::TextUnformatted(" pixels.");
                ImGui::RadioButton("Keep aspect ratio and set height to", &export_bitmap_scale_selection, 3);
                  ImGui::BeginDisabled(export_bitmap_scale_selection!=3);
                  ImGui::SameLine();
                  ImGui::SetNextItemWidth(30*Config.dpi_mul);
                  ImGui::InputInt("##ysize", &y, 0, 0);
                  ImGui::EndDisabled();
                  ImGui::SameLine();
                  ImGui::TextUnformatted(" pixels.");
                ImGui::RadioButton("Set width to", &export_bitmap_scale_selection, 4);
                  ImGui::BeginDisabled(export_bitmap_scale_selection!=4);
                  ImGui::SameLine();
                  ImGui::SetNextItemWidth(30*Config.dpi_mul);
                  ImGui::InputInt("##xsize2", &x, 0, 0);
                  ImGui::EndDisabled();
                  ImGui::SameLine();
                  ImGui::TextUnformatted(" pixels and height to");
                  ImGui::BeginDisabled(export_bitmap_scale_selection != 4);
                  ImGui::SameLine();
                  ImGui::SetNextItemWidth(30*Config.dpi_mul);
                  ImGui::InputInt("##ysize2", &y, 0, 0);
                  ImGui::EndDisabled();
                  ImGui::SameLine();
                  ImGui::TextUnformatted(" pixels.");
                ImGui::Spacing();
                ImGui::Checkbox("Include chart text into PNG", &export_png_include_chart_text);
                ignore_pagebreaks = int(export_ignore_pagebreaks);
                ImGui::BeginDisabled(C->pChart->GetPageNum()<2);
                ImGui::RadioButton("Export each page to a separate PNG file", &ignore_pagebreaks, 0);
                ImGui::RadioButton("Export all pages into a single PNG image", &ignore_pagebreaks, 1);
                ImGui::EndDisabled();
                export_ignore_pagebreaks = bool(ignore_pagebreaks);

                ImGui::SetItemDefaultFocus();
                if (ImGui::Button("OK") || ImGui::IsKeyPressed(ImGuiKey_Enter)) {
                    ImGui::CloseCurrentPopup();
                    switch (export_bitmap_scale_selection) {
                    default: _ASSERT(0); [[fallthrough]];
                    case 0: sc = {1,1}; break;
                    case 1: sc = {export_bitmap_scale_factor, export_bitmap_scale_factor}; break;
                    case 2: sc.y = sc.x = x/C->pChart->GetTotal().y.Spans(); break;
                    case 3: sc.y = sc.x = y/C->pChart->GetTotal().x.Spans(); break;
                    case 4: sc = {x/C->pChart->GetTotal().y.Spans(), y/C->pChart->GetTotal().x.Spans()}; break;
                    }
                    ipbrk = export_ignore_pagebreaks;
                    status = SHOW_ERROR;

                }
                ImGui::SameLine();
                if (ImGui::Button("Cancel") || escape_pressed) {
                    ImGui::CloseCurrentPopup();
                    status = CANCELLED;
                }
                ImGui::EndPopup();
            }
            ImGui::PopStyleColor();
            break;
        case Canvas::PPT:
        case Canvas::PDF:
            ImGui::OpenPopup(_type==Canvas::PPT ? "PPT Options" : "PDF options");
            ImGui::PushStyleColor(ImGuiCol_PopupBg, IM_COL32(240, 240, 240, 255));
            if (ImGui::BeginPopupModal(_type==Canvas::PPT ? "PPT Options" : "PDF options", nullptr,
                                       ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoResize)) {
                const char *ext = _type==Canvas::PPT ? "PPT" : "PDF";
                const char *slide = _type==Canvas::PPT ? "slide" : "PDF page";
                PPTPDFExportOptions& fmt = _type==Canvas::PPT ? export_ppt : export_pdf;
                ImGui::BeginDisabled(C->pChart->GetPageNum()<=1);
                ImGui::Text("Please select how to export multiple pages.");
                if (_type==Canvas::PDF)
                    ImGui::RadioButton(StrCat("The whole chart in one ", ext, " file on one page that fits the chart.").c_str(), &fmt.multipage_mode, -1);
                ImGui::RadioButton(StrCat("The whole chart in one ", ext, " file on one fix-sized ", slide, ".").c_str(), &fmt.multipage_mode, 0);
                ImGui::RadioButton(StrCat("Single file, each chart page on a separate ", slide, ".").c_str(), &fmt.multipage_mode, 1);
                if (_type==Canvas::PDF)
                    ImGui::RadioButton(StrCat("Each chart page in a separate ", ext, " file.").c_str(), &fmt.multipage_mode, 2);
                ImGui::EndDisabled();
                ImGui::Separator();
                const bool fix_page_size = fmt.multipage_mode != -1 && fmt.multipage_mode != 2;
                ImGui::BeginDisabled(!fix_page_size);
                ImGui::Combo("Page size##PPT Page size", &fmt.page_size, page_sizes.data(), page_sizes.size());
                ImGui::EndDisabled();
                ps = fix_page_size ? GetPhysicalPageSize(EPageSize(fmt.page_size+unsigned(EPageSize::NO_PAGE)+1)) : XY(0, 0);
                ImGui::TextUnformatted("Scale");
                ImGui::BeginDisabled(!fix_page_size);
                ImGui::RadioButton(StrCat("Scale to fit the largest page to the ", slide, " size.").c_str(), &fmt.scale_mode, 0);
                ImGui::RadioButton(StrCat("Scale to fit the widest page to the ", slide, " width.").c_str(), &fmt.scale_mode, 1);
                ImGui::RadioButton("Scale by", &fmt.scale_mode, 2);
                    ImGui::SameLine();
                    ImGui::BeginDisabled(fmt.scale_mode!=2);
                    if (fmt.scale<0) fmt.scale = -fmt.scale; //force nonnegative
                    ImGui::InputDouble("##export_scale", &fmt.scale, 0.f, 0.f, "%.2g");
                    ImGui::EndDisabled();
                ImGui::EndDisabled();
                const PBDataVector pages = C->pChart->GetPageVector();
                static const auto XSize = [](const PageBreakData* p) { return p->wh.x+p->headingLeftingSize.x; };
                static const auto YSize = [](const PageBreakData* p) { return p->wh.y+p->headingLeftingSize.y; };
                const XY max_page_size = {
                    XSize(std::ranges::max(pages, {}, XSize)),
                    YSize(std::ranges::max(pages, {}, YSize)) };
                XY slide_size = ps;
                slide_size.x -= fmt.margins[0] + fmt.margins[1];
                slide_size.y -= fmt.margins[2] + fmt.margins[3];
                const double fit_width_scale = slide_size.x/max_page_size.x;
                const double fit_all_scale = std::min(fit_width_scale, slide_size.y/max_page_size.y);
                if (fix_page_size) {
                    if (slide_size.x>10 && slide_size.y>10) {
                        ImGui::Text("Recommended max scale: %.2g", fit_all_scale);
                        Help(StrCat("This is the max scale that will fit all pages to the ", slide, "size with the margins given.").c_str());
                    } else {
                        ImGui::Text("Recommended max scale: ---");
                        Help("Too large margins, no space left for chart.");
                    }
                }
                fmt.scale = fmt.scale_mode==0 ? fit_all_scale : fmt.scale_mode ==1 ? fit_width_scale : between(fmt.scale, 0.1, 10.);
                ImGui::Separator();
                ImGui::BeginDisabled(!fix_page_size);
                if (ImGui::BeginTable("##PPT page options", 3, ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_BordersInnerV)) {
                    ImGui::TableNextRow();
                    ImGui::TableNextColumn();
                        ImGui::Text("Horizontal");
                        ImGui::Text("Alignment");
                        ImGui::RadioButton("Left##alignment", &fmt.ha, -1);
                        ImGui::RadioButton("Center##alignment", &fmt.ha, 0);
                        ImGui::RadioButton("Right##alignment", &fmt.ha, +1);
                    ImGui::TableNextColumn();
                        ImGui::Text("Vertical");
                        ImGui::Text("Alignment");
                        ImGui::RadioButton("Top##alignment", &fmt.va, -1);
                        ImGui::RadioButton("Middle##alignment", &fmt.va, 0);
                        ImGui::RadioButton("Bottom##alignment", &fmt.va, +1);
                    ImGui::TableNextColumn();
                        ImGui::Text("Margins in:");
                        ImGui::SameLine();
                        ImGui::SetNextItemWidth(dpi_mul*50);
                        ImGui::Combo("##incmpoints", &fmt.unit, "in\0cm\0pt\0");
                        static constexpr double points[3] = { 72., 72/2.54, 1. }; //how many points an inch, cm or point have
                        for (unsigned u = 0; u<4; u++)
                            ppt_gui_margins[u] = (float)(fmt.margins[u]/points[fmt.unit]);
                        ImGui::SetNextItemWidth(dpi_mul*50);
                        ImGui::InputFloat("Left##margin", &ppt_gui_margins[0], 0.f, 0.f, "%g");
                        ImGui::SetNextItemWidth(dpi_mul*50);
                        ImGui::InputFloat("Right##margin", &ppt_gui_margins[1], 0.f, 0.f, "%g");
                        ImGui::SetNextItemWidth(dpi_mul*50);
                        ImGui::InputFloat("Top##margin", &ppt_gui_margins[2], 0.f, 0.f, "%g");
                        ImGui::SetNextItemWidth(dpi_mul*50);
                        ImGui::InputFloat("Bottom##margin", &ppt_gui_margins[3], 0.f, 0.f, "%g");
                        for (unsigned u = 0; u<4; u++)
                            fmt.margins[u] = ppt_gui_margins[u]*points[fmt.unit];
                        if (fix_page_size) {
                            if (slide_size.x<10)
                                ImGui::TextColored(ImVec4{ 1,0,0,1 }, "Too little X space left!");
                            else if (slide_size.y<10)
                                ImGui::TextColored(ImVec4{ 1,0,0,1 }, "Too little Y space left!");
                        }
                        ImGui::EndTable();
                }
                ImGui::EndDisabled();
                ImGui::SetItemDefaultFocus();
                if (ImGui::Button("OK") || ImGui::IsKeyPressed(ImGuiKey_Enter)) {
                    ImGui::CloseCurrentPopup();
                    ha = fmt.ha;
                    va = fmt.va;
                    pMargins = fmt.margins.data();
                    sc = fix_page_size ? XY(fmt.scale, fmt.scale) : XY(1, 1);
                    ipbrk = fmt.multipage_mode <= 0;
                    status = SHOW_ERROR;
                }
                ImGui::SameLine();
                if (ImGui::Button("Cancel") || escape_pressed) {
                    ImGui::CloseCurrentPopup();
                    status = CANCELLED;
                }
                ImGui::EndPopup();
            }
            ImGui::PopStyleColor();
            break;

        case Canvas::EPS:
        case Canvas::SVG:
            if (C->pChart->GetPageNum()>1) {
                const char* ext = _type==Canvas::EPS ? "EPS" : "SVG";
                ImGui::OpenPopup(StrCat(ext, " Options").c_str());
                ImGui::PushStyleColor(ImGuiCol_PopupBg, IM_COL32(240, 240, 240, 255));
                if (ImGui::BeginPopupModal(StrCat(ext, " Options").c_str(), nullptr,
                                           ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoResize)) {
                    ignore_pagebreaks = int(export_ignore_pagebreaks);
                    ImGui::RadioButton(StrCat("Export each page to a separate ", ext, " file").c_str(), &ignore_pagebreaks, 0);
                    ImGui::RadioButton(StrCat("Export all pages into a single ", ext, " file").c_str(), &ignore_pagebreaks, 1);
                    export_ignore_pagebreaks = ignore_pagebreaks;
                    ImGui::SetItemDefaultFocus();
                    if (ImGui::Button("OK") || ImGui::IsKeyPressed(ImGuiKey_Enter)) {
                        ImGui::CloseCurrentPopup();
                        ipbrk = export_ignore_pagebreaks;
                        status = SHOW_ERROR;
                    }
                    ImGui::SameLine();
                    if (ImGui::Button("Cancel") || escape_pressed) {
                        ImGui::CloseCurrentPopup();
                        status = CANCELLED;
                    }
                    ImGui::EndPopup();
                }
                ImGui::PopStyleColor();
            } else
                status = SHOW_ERROR;
            break;
        default:
            status = CANCELLED;
        }
        if (status == SHOW_ERROR) {
            AddRecentFile(RecentFile{std::string(DirNameFromPath(_full_path)), ""});
            const unsigned err = C->pChart->Error.GetErrorNum(false, false);
            if (C->pChart->DrawToFile(_type, sc, _full_path, Settings.show_page_breaks, ipbrk,
                                      export_png_include_chart_text ? C->editor.GetText().c_str() : nullptr,
                                      0, ps, pMargins, ha, va, true)) {
                if (reuse_last_name)
                    AddMessage({"Export complete."});
                status = INITIAL;
            } else {
                if (err==C->pChart->Error.GetErrorNum(false, false)) {
                    error = "Could not export the chart (and cannot tell why).";
                } else {
                    error = C->pChart->Error.GetErrorMessage(err, false, false);
                    while (err < C->pChart->Error.GetErrorNum(false, false))
                        C->pChart->Error.DeleteError(err, false, false);
                }
            }
        }
    }
    if (status == SHOW_ERROR)
        if (GUIError(error.c_str())) //do not show error on quiet
            status = INITIAL;
    switch (status) {
    case INITIAL: return StatusRet::Completed; //if we are set back to initial, we are done.
    case CANCELLED: status = INITIAL; return StatusRet::Cancelled;
    default: return StatusRet::Ongoing;
    }
}

int get_indent(const TextEditor::Line &L) noexcept {
    int spaces = 0;
    while (spaces<(int)L.size() && (L[spaces].mChar==' ' || L[spaces].mChar=='\t'))
        spaces++;
    return spaces;
}


int EditorChartData::SmartIndent(TextEditor::Lines &lines, char character, bool /*shift*/,
                                 const TextEditor::Coordinates& from, const TextEditor::Coordinates& till) {
    int inserted = 0;
    CharByteIndex pos;
    for (int l = 0; l < from.mLine; l++) {
        pos.char_index += lines[l].num_chars() + 1; //+1 for newline
        pos.byte_index += lines[l].size() + 1; 
    }
    switch (character) {
    case '\t':
        for (int l = from.mLine; l<=till.mLine; l++) {
            const int spaces = hint.csh->FindProperLineIndent(pos, false) - get_indent(lines[l]);
            pos.char_index += spaces + lines[l].num_chars() + 1;
            pos.byte_index += spaces + lines[l].size() + 1;
            if (spaces==0) continue;
            if (spaces>0) lines[l].insert(lines[l].begin(), spaces, TextEditor::Glyph{' '});
            else lines[l].erase(lines[l].begin(), lines[l].begin()-spaces);
            lines[l].invalidate_num_chars();
            inserted = spaces; 
            if (l<till.mLine)
                Colorize(lines); //Recalculate CSH values, except at the last line=>we need them to ProperLineIndent subsequent lines
        }
        break;
    case '\x08': //backspace
        if (from.mColumn || get_indent(lines[from.mLine])<from.mColumn) //if at beginning of line or not in leading whitespace
            return 0;
        else if (const int needed = hint.csh->FindProperLineIndent(pos, false)
                    , current = get_indent(lines[from.mLine])
                ; needed >= current) return 0;
        else
            return current-needed;
    case '{':
    case '}':
    case '[':
    case ']':
        if (get_indent(lines[from.mLine])<from.mColumn-1) //newly inserted char is not the first in line
            break;
        FALLTHROUGH;
    case '\n':
        inserted = hint.csh->FindProperLineIndent(pos, character=='\n') - get_indent(lines[from.mLine]);
        if (inserted==0) break;
        if (inserted>0) lines[from.mLine].insert(lines[from.mLine].begin(), inserted, TextEditor::Glyph{' '});
        else lines[from.mLine].erase(lines[from.mLine].begin(), lines[from.mLine].begin()-inserted);
        lines[from.mLine].invalidate_num_chars();
    }
    return inserted;
}

enum class EAdvanceMode { 
    AlwaysOneChar,           ///<Go to the next line only if the next char is a newline
    SwallowOneExtraNewline,  ///<If the next char is the last in the line, step over it and one newline, as well. If at a newline, just step over than one newline
    SwallowAllExtraNewlines  ///<If the next char is the last in the line, step over it and all consecutive newlines. If at a newline, step over all consecutive newlines.
};

/* Moves line/col and pos one character ahead.
 * For mode==SwallowAllExtraNewlines, we ensure that col<lines[line].size() always holds, except at the end of the file.
 * For other modes, col may equal lines[line].size()
 * @returns true if we have reached the end of 'lines'*/
inline bool Advance(const TextEditor::Lines &lines, int &line, int &col, int &pos, EAdvanceMode mode) {
    _ASSERT(line<(int)lines.size());
    if (col < (int)lines[line].size()) { //If at a line-end, just skip the newline(s) below
        ++pos;
        col += UTF8TrailingBytes(lines[line][col].mChar)+1;
        if (mode== EAdvanceMode::AlwaysOneChar) return (int)lines.size()==line+1 && int(lines[line].size())==col;
    }
    while (line < (int)lines.size() && (int)lines[line].size() <= col) { //cycle to process end-of line and also empty lines
        ++line; col = 0; //jump to new line
        pos++; //step over the newline in the input
        if (mode!=EAdvanceMode::SwallowAllExtraNewlines) break;
    }
    return (int)lines.size() <= line;
}

void EditorChartData::Colorize(TextEditor::Lines &lines) {
    if (!lang || !lang->pCsh) return;
    const auto started = ConfigData::clock::now();
    hint.csh = lang->pCsh->Clone();
    hint.csh->FileName = Config.file_string();
    const auto [chars, bytes] = editor.GetCharBytePos(editor.GetCursorPosition());
    hint.csh->ParseText(editor.GetText(), CharByteIndex{}, CharByteIndex{chars, bytes}, Settings.pedantic);
    hint.csh->CshList.SortByPos();
    int line = 0, col = 0, pos = 0;
    //Step through potential empty lines at the beginning of the file  
    while (line < (int)lines.size() && lines[line].empty()) {
        line++;
        pos++;
    }
    for (const CshEntry& c : hint.csh->CshList) {
        while (pos < c.first_pos.char_index) {
            lines[line][col].mColorIndex = COLOR_NORMAL;
            if (Advance(lines, line, col, pos, EAdvanceMode::SwallowAllExtraNewlines)) goto done;
        }
        while (pos<c.end_pos.char_index) {
            lines[line][col].mColorIndex = c.color;
            if (Advance(lines, line, col, pos, EAdvanceMode::SwallowAllExtraNewlines)) goto done;
        }
    }
    //Advance to the end of the file and clear all formatting
    while (line < (int)lines.size() && !Advance(lines, line, col, pos, EAdvanceMode::SwallowAllExtraNewlines))
        if (col < (int)lines[line].size())
            lines[line][col].mColorIndex = COLOR_NORMAL;
done:
    TextEditor::ErrorMarkers errors;
    if (Config.error_squiggles) {
        line = 0, col = 0, pos = 0;
        for (const CshError& c : hint.csh->CshErrors.error_ranges) {
            bool at_end = lines.empty();
            while (!at_end && pos < c.first_pos.char_index)
                at_end = Advance(lines, line, col, pos, EAdvanceMode::AlwaysOneChar);
            TextEditor::Coordinates start(line, col);
            int start_pos = pos;
            if (at_end && pos == c.end_pos.char_index) {//if an error starts just after the chart text, we show a 1-char squiggle
                errors[start] = std::pair(1, c.text);
                goto done_errors;
            }
            while (pos < c.end_pos.char_index && line < (int)lines.size())
                if (Advance(lines, line, col, pos, EAdvanceMode::AlwaysOneChar)) {
                    if (start_pos < pos) //an error ending at or beyond the end of the file
                        errors[start] = std::pair(pos - start_pos, c.text);
                    goto done_errors;
                } else if (col >= (int)lines[line].size() //Newline encountered - errors are marked only until the end of the line
                         && start_pos < pos) {            //non-empty erroneous part
                    errors[start] = std::pair(pos - start_pos, c.text);
                    start = TextEditor::Coordinates(line, col);
                    start_pos = pos;
                }
            if (start_pos<pos) //non-empty erroneous part remained
                errors[start] = std::pair(pos-start_pos, c.text);
        }
    }
done_errors:
    editor.SetErrorMarkers(std::move(errors));
    Settings.last_csh_time = std::chrono::duration_cast<std::chrono::milliseconds>(ConfigData::clock::now()-started);
    if (hint.active && !ProcessHints(*hint.csh))
        HintCancel(true);
}

void EditorChartData::StartCompile() {
    selected_error = -1;
    if (!lang) return;
    std::unique_ptr<Chart> chart = lang->create_chart(Config.G->file_read_proc, Config.G->file_read_proc_param);
    if (!chart) return;
    chart->prepare_for_tracking = true;
    chart->prepare_element_controls = true;
    //add shapes
    chart->Shapes = lang->shapes;
    //add errors
    chart->Error = lang->designlib_errors;
    //add designs
    chart->ImportDesigns(lang->designs);
    //Add chart arguments
    for (auto& [arg, langs] : Config.G->command_line_arguments) //TODO: avoid using the 'Config' singleton
        chart->AddCommandLineArg(arg);
    //add forced design
    if (Settings.forced_design.size())
        chart->ApplyForcedDesign(Settings.forced_design);
    //add chart options specified on the command-line
    for (auto& [attr, langs] : Config.G->command_line_options) //TODO: avoid using the 'Config' singleton)
        if (std::ranges::find(langs, lang->GetName()) != langs.end())
            chart->AddCommandLineOption(attr);
    //set copyright text
    chart->copyrightText = Config.G->copyright_text;
    //Set default font, requested by the user
    chart->SetDefaultFont(Config.G->fontface, Config.G->fontlang, {}); //TODO: avoid using the 'Config' singleton

    //MscChart specific settings
    if (msc::MscChart* msc = dynamic_cast<MscChart*>(chart.get())) {
        //copy pedantic flag from app settings
        msc->mscgen_compat = EMscgenCompat::AUTODETECT;
        msc->Error.warn_mscgen = true;
    }
    //Graph specific settings
    if (graph::GraphChart* graph = dynamic_cast<GraphChart*>(chart.get())) {
        graph->do_orig_lang = false;
        //set layout after setting the default style - this overrides that
        if (Settings.forced_layout.size())
            graph->ApplyForcedLayout(Settings.forced_layout);
    }
    chart->SetPedantic(pedantic.value_or(Settings.pedantic));
    chart->GetProgress()->ReadLoadData(Config.load_data[lang->GetName()]);
    //copy forced collapse/expand entities/boxes/graphs, etc.
    chart->DeserializeGUIState(control_state);

    compile_data.Start(std::move(chart), editor.GetText(), Config.file_string(true), editor.GetUndoBufferIndex());
}

bool EditorChartData::CompileIfNeeded() {
    if (compile_data.is_compiling_at(editor.GetUndoBufferIndex())) return false;
    if (pChart && compiled_at == editor.GetUndoBufferIndex()) return true;
    StartCompile(); //result will be taken in the main loop
    return false;
}


void CompileData::Start(std::unique_ptr<Chart> &&chart, std::string &&text, const std::string &fn,
                        TextEditor::UndoBufferIndex editorbuffer) {
    Stop();
    if (destroying.load(std::memory_order_acquire))
        return; //we do not accept any new threads if we are destroying
    if (auto s = state.load(std::memory_order_acquire)
            ; result.compiled_at == editorbuffer
              && (s==State::Compiling || s==State::Ready))
        return;      //already started or finished compiling this editor state
    //Cancel ongoing compilation. If we have succeeded in the meantime,
    //we just drop the result, as we need a different editor state.
    GetResult(true);
    std::scoped_lock lock(my_mutex);
    if (state.load(std::memory_order_acquire)!=State::Idle)
        return;
    state.store(State::Compiling, std::memory_order_release);
    compiling_started_ended = clock::now();

    result.pChart = std::move(chart);
    result.compiled_at = editorbuffer;

    compile_thread = std::thread([this, text = std::move(text), fn]{
        auto started = clock::now();
        //parse chart text
        result.pChart->GetProgress()->callback = progressbar;
        result.pChart->GetProgress()->data = this;
        try {
            result.pChart->GetProgress()->StartParse();
            result.main_file_no = result.pChart->ParseText(text, fn);
            if (!result.pChart->Error.hasFatal())
                //Do post-parse, compile, calculate sizes and sort errors by line
                result.pChart->CompleteParse(/* autopaginate =*/ false,
                                             false, XY(0, 0), false, true);
            if (!result.pChart->Error.hasFatal()) {
                Canvas canvas(Canvas::Empty::Query);
                result.pChart->CollectIsMapElements(canvas);
            }
            result.pChart->Error.RemovePathFromErrorMessages();
            result.pChart->GetProgress()->Done();
            state.store(State::Ready, std::memory_order_release);
        } catch (const AbortCompilingException &) {
            state.store(State::Stopped, std::memory_order_release);

        }
        result.compile_time = clock::now()-started;
    });
}

std::optional<CompileData::Result> CompileData::GetResult(bool block) noexcept {
    switch (state.load(std::memory_order_acquire)) {
    case State::Stopped:
    {
        if (compile_thread.joinable())
            compile_thread.join();
        compile_thread = {};
        std::scoped_lock lock(my_mutex);
        state.store(State::Idle, std::memory_order_release);
        stop.store(false, std::memory_order_release);
        compiling_started_ended = clock::now();
        result.pChart.release();
        return {};
    }
    default:
    case State::Idle:
        stop.store(false, std::memory_order_release);
        return {};
    case State::Compiling:
        if (!block) return {};
        FALLTHROUGH;
    case State::Ready:
        if (compile_thread.joinable())
            compile_thread.join();
        compile_thread = {};
        std::scoped_lock lock(my_mutex);
        stop.store(false, std::memory_order_release);
        compiling_started_ended = clock::now();
        if (state.exchange(State::Idle, std::memory_order_acq_rel) == State::Ready)
            return std::move(result);
        result.pChart.release();
        return {};
    }
}

bool EditorChartData::CompileTakeResult(bool block) {
    auto result = compile_data.GetResult(block);
    if (!result) return false;
    Config.load_data[lang->GetName()] = result->pChart->GetProgress()->WriteLoadData();
    if (Config.G->load_data) *Config.G->load_data = PackLoadData(Config.load_data);
    control_state = result->pChart->SerializeGUIState();
    Settings.last_compile_time = std::chrono::duration_cast<std::chrono::milliseconds>(result->compile_time);
    result->pChart->Error.TechnicalInfo({}, StrCat("Compilation time was: ", std::to_string(Settings.last_compile_time.count()), "ms"));
    const auto res = result->pChart->GetProgress()->GetUnifiedSectionPercentage();
    //If we did not end on 100%, scale up, so that printed results sum up to 100%
    const double ratio = 100./result->pChart->GetProgress()->GetPercentage();
    for (unsigned u = 0; u<res.size(); u++)
        if (int(round(res[u]*ratio)))
            result->pChart->Error.TechnicalInfo(
                {}, StrCat(result->pChart->GetProgress()->GetUnifiedSectionName(u), ":",
                           std::to_string(int(round(res[u]*ratio))), "% ",
                           std::to_string(int(round(res[u]*ratio*Settings.last_compile_time.count()/100.))), "ms"));
    pChart = std::move(result->pChart);
    main_file_no = result->main_file_no;
    compiled_at = result->compiled_at;
    ResetCompileCache();
    overlays.clear();
    controls.clear();
    if (page>pChart->GetPageNum()) page = pChart->GetPageNum();
    return true;
}


void EditorChartData::DrawCache() {
    if (!pChart) return;
    if (!compile_cache.chart) {
        const PBDataVector pageBreakData = pChart->GetPageVector();
        Block b = page==0 ? pChart->GetTotal() : Block(pageBreakData[page-1]->xy, pageBreakData[page-1]->xy + pageBreakData[page-1]->wh);
        if (page && pageBreakData[page-1]->headingLeftingSize.y>0) b.y.from -= pageBreakData[page-1]->headingLeftingSize.y;
        if (page && pageBreakData[page-1]->headingLeftingSize.x>0) b.x.from -= pageBreakData[page-1]->headingLeftingSize.x;
        if (b.IsInvalid() || (int)b.x.Spans()<2 || (int)b.y.Spans()<2) return;
        b.y.till += pChart->GetCopyrightTextHeight();
        b.Scale(zoom);

        cairo_surface_t *surf = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, (int)b.x.Spans(), (int)b.y.Spans());
        cairo_status_t status = cairo_surface_status(surf);
        if (status != CAIRO_STATUS_SUCCESS) {
            cairo_surface_destroy(surf);
            return;
        }
        Canvas canvas(Canvas::PNG, surf, pChart->GetTotal(), pChart->GetCopyrightTextHeight(), XY(zoom, zoom), &pageBreakData, page);
        if (canvas.ErrorAfterCreation(&pChart->Error, &pageBreakData)) {
            cairo_surface_destroy(surf);
            return;
        }
        pChart->DrawComplete(canvas, true, page);
        if (TextureID texture = TextureFromSurface(surf))
            compile_cache.chart.emplace(std::move(texture), ImVec2((float)b.x.Spans(), (float)b.y.Spans()));
        //destroying canvas, will destroy the cairo surface
    }
    const ImVec2 ctrl_size = ImVec2(float(Element::control_size.x), float(Element::control_size.x))*zoom;
    for (unsigned u = 1; u<int(EGUIControlType::MAX); u++) {
        const EGUIControlType type = EGUIControlType(u);
        std::optional<Texture>& ctrl = compile_cache.controls[u];
        if (ctrl && ctrl->size!=ctrl_size) ctrl.reset();
        if (!ctrl) {
            cairo_surface_t* surf = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, (int)ctrl_size.x, (int)ctrl_size.y);
            cairo_status_t status = cairo_surface_status(surf);
            if (status != CAIRO_STATUS_SUCCESS) {
                cairo_surface_destroy(surf);
                continue;
            }
            cairo_t* cr = cairo_create(surf);
            cairo_scale(cr, zoom, zoom);
            Element::DrawControl(cr, type, 1.0);
            cairo_destroy(cr);
            if (TextureID texture = TextureFromSurface(surf))
                ctrl.emplace(std::move(texture), ctrl_size);
            cairo_surface_destroy(surf);
        }
    }
    //Draw overlays to texture
    for (Overlay &o: overlays) if (!o.texture) {
        Contour tmp_storage;
        const Contour& area = o.element.GetAreaToDraw(tmp_storage);
        Block b = area.GetBoundingBox();
        if (b.IsInvalid() || (int)b.x.Spans()<2 || (int)b.y.Spans()<2) continue;
        b.Expand(5); //If the outline draw ends up going a bit outside the bounding box (e.g., miters, etc.)
        b.Scale(zoom);
        cairo_surface_t *surf = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, (int)b.x.Spans(), (int)b.y.Spans());
        cairo_status_t status = cairo_surface_status(surf);
        if (status != CAIRO_STATUS_SUCCESS) {
            cairo_surface_destroy(surf);
            continue;
        }
        cairo_t *cr = cairo_create(surf);
        cairo_translate(cr, -b.x.from, -b.y.from);
        cairo_scale(cr, zoom, zoom);
        area.CairoPath(cr, false);
        cairo_set_source_rgba(cr, 1, 0.4, 0.4, 0.5);
        cairo_fill_preserve(cr);
        cairo_set_source_rgba(cr, 0.7, 0.2, 0.2, 1);
        cairo_stroke(cr);
        cairo_destroy(cr);
        cairo_surface_flush(surf);
        if (TextureID texture = TextureFromSurface(surf)) {
            o.texture.emplace(std::move(texture), (ImVec2)b.Spans());
            o.offset = (ImVec2)(b.UpperLeft()-pChart->GetTotal().UpperLeft()*zoom);
        }
        cairo_surface_destroy(surf);
    }
}
void EditorChartData::Animate(const ElementRef &element, float initial_fade, bool do_fade_out) {
    auto i = std::ranges::find_if(overlays, [&element](const Overlay &o) {return o.element.element==element.element; });
    if (i==overlays.end()) {
        overlays.emplace_back(element, initial_fade, do_fade_out);
        DrawCache(); //will only touch the last we just added
    } else {
        i->fade = initial_fade;
        i->do_fade_out |= do_fade_out;
    }
}

void EditorChartData::PruneAnimations() {
    //Prune animation that has finished.
    std::erase_if(overlays, [this](const Overlay& o) { return o.fade==0; });
}

void EditorChartData::ShowControls(const ElementRef &element) {
    auto i = std::ranges::find_if(controls, [&element](const Control& c) {return c.element.element==element.element; });
    if (i==controls.end()) {
        controls.emplace_back(element, 0.1f);
    } else {
        i->size = anim_time_value(i->size, 1, 10);
    }
}

void EditorChartData::ControlClicked(const ElementRef& element, EGUIControlType t) {
    if (!element) return;
    if (!pChart) return;
    if (t==EGUIControlType::INVALID) return;
    Chart* chart_to_use = element.inline_data ? element.inline_data->chart : pChart.get();
    if (chart_to_use->ControlClicked(element.element, t)) {
        chart_to_use->GUIStateChanged(); //this recalculates the GUI changes in all child inlined charts
        //OK, something changed. Take new GUI state...
        control_state = pChart->SerializeGUIState();
        StartCompile();
    }
}

//We don't destroy the controls - on redraw we check if their size has changed
void EditorChartData::ResetCompileCache() {
    compile_cache.chart.reset();
    for (auto &o : overlays)
        o.texture.reset();
    PruneAnimations();
}

bool eligible_for_autohint(char c) {
    return std::isalnum(c) || c == '.' || c == '_' || c == '[' || c == '=' || c == '+' || c=='*';
}

void EditorChartData::PostEditorRender(const TextEditor::UndoBufferIndex &undo_status_before) {
    if (int index = editor.UndoBufferClearedFrom(); index>=0)
        if (index<=compiled_at.mIndex) compiled_at = {};
    if (auto& io = ImGui::GetIO();  ImGui::IsItemHovered() && io.MouseWheel && io.KeyCtrl)
        SetFontScale(Settings.editor_font_scale*pow(1.1f, between(io.MouseWheel, -5.f, +5.f)));
    if (int index = editor.UndoBufferClearedFrom(); index>=0)
        if (index<=saved_at.mIndex) saved_at = {};

    //With auto_hint, turn hinting automatically on if a letter or [=._*+ is pressed
    if (hint.csh && !hint.active && Config.auto_hint)
        if (const auto undo_status = editor.GetUndoBufferIndex(); undo_status != undo_status_before)
            if (const auto undo = editor.GetLastUndoRecord())
                if (undo->added.length() == 1 && eligible_for_autohint(undo->added.front())) {
                    hint.active = true;
                    hint.is_user_requested = false;
                    //if the char was pressed in the middle of a word, hint and replace the entire word
                    //if the char was pressed at the beginning of a word, hint and replace only the portion before the cursor
                    hint.replace_till_cursor = hint.search_till_cursor = hint.csh->hintedStringPos.first_pos.char_index < editor.GetCharPos(editor.GetCursorPosition());
                    if (!ProcessHints(*hint.csh))
                        HintCancel(true);
                }
}


void EditorChartData::NavigateErrors(bool up, bool detailed) noexcept {
    if (!pChart) return;
    const int errnum = pChart->Error.GetErrorNum(Settings.warnings, Settings.technical_info);
    if (errnum==0) return;
    if (selected_error<0) selected_error = up ? errnum : -1;
    const int started_at = selected_error;
    do
        selected_error = (selected_error + (up ? -1 : +1) + errnum)%errnum;
    while (selected_error!=started_at
            && ((!detailed
                && pChart->Error.GetError(selected_error, Settings.warnings, Settings.technical_info)->line_type != ErrorElement::ELineType::Main)
                || pChart->Error.GetError(selected_error, Settings.warnings, Settings.technical_info)->relevant_line.file != main_file_no));
    GotoError();
}
float EditorChartData::GetHeadingSize() noexcept {
    if (!Config.auto_heading) return 0;
    //This is MscChart specific
    MscChart *msc = dynamic_cast<MscChart *>(pChart.get());
    if (msc==nullptr) return 0;
    if (page<=1 || page > msc->GetPageNum())
        return zoom*float(msc->GetHeadingSize() - msc->GetTotal().x.from);
    return zoom*float(msc->GetPageData(page-1)->headingLeftingSize.y);
}

std::optional<XY> EditorChartData::MouseToChart(const ImVec2& mouse) const noexcept {
    if (!pChart) return {};
    if (page>pChart->GetPageNum()) return {};
    XY PageOffset{0,0};
    if (page)
        if (const auto* pData = pChart->GetPageData(page))
            PageOffset = pData->xy;
    return (XY)mouse/zoom + PageOffset + pChart->GetTotal().UpperLeft();
}

ElementRef EditorChartData::GetArc(const ImVec2 &mouse) const {
    const auto M = MouseToChart(mouse);
    if (!M) return {};
    if (!pChart->GetTotal().IsWithinBool(*M)) return {};
    return pChart->FindElement(*M);
}

ElementRef EditorChartData::GetArc(const TextEditor::Coordinates &pos) const  {
    if (tracking_mode) return {};
    if (!pChart || !IsCompiled()) return {};
    FileLineCol linenum(int(pChart->Error.Files.size()-1), pos.mLine+1, pos.mColumn+1);
    return pChart->FindElement(linenum);
}


bool EditorChartData::SetCursorTo(const ElementRef& E) {
    if (!E) return false;
    const FileLineColRange &P = E.element->file_pos;
    if (P.start.file != main_file_no || P.end.file != main_file_no) return false;
    const TextEditor::Coordinates s1{(int)P.start.line-1, (int)P.start.col-1};
    const TextEditor::Coordinates s2{(int)P.end.line-1, (int)P.end.col-1}; 
    editor.SetCursorPosition(s1);
    editor.SetSelection(s1, s2);
    return true;
}

void EditorChartData::ResetToNew(const std::string &text) {
    editor.SetText(text);
    slide = chart = 0;
    StartCompile();
}

void EditorChartData::ResetText(const LanguageData* l, const std::string* text) {
    lang = l;
    ResetToNew(text ? *text : lang->default_text);
    control_state = {};
    saved_at = editor.GetUndoBufferIndex();
    page = 0;
}

//Process hints after a re-coloring
//returns false if we can cancel the hints (or was not active)
bool EditorChartData::ProcessHints(Csh &csh) {
    //destroy the old graphics
    hint.items.clear();
    hint.empty_graphics.clear();
    //cancel hints if
    if (editor.HasSelection()                  //1. multiple characters selected; or
           || csh.Hints.size()==0) {           //2. no hints collected; or
    cancel:
        csh.Hints.clear();
        return false;
    }
    const CshPos &p = csh.hintedStringPos;                            
    const int start = editor.GetCharPos(editor.GetCursorPosition());  
    if (start<0) goto cancel;                  //3. caret is outside the text (how can that be)?; or
    if (p.char_length() == 1                   //4. the current hint is the previous autocompletion (initiated from this function below) (because the hinted string is not one character long)...
            && p.first_pos.char_index == start //   ... and the cursor stands at the beginning of a real nonzero len hinted word ...
            && !hint.is_user_requested)        //   ... and the used did not press Ctrl+Space.
        goto cancel;

    std::string under_cursor;
    if (!csh.hintedStringPos.empty()) {
        const int search_end_pos = hint.search_till_cursor
            ? std::min(start, csh.hintedStringPos.end_pos.char_index)
            : csh.hintedStringPos.end_pos.char_index;
        under_cursor = editor.GetText(editor.GetCoordinate(csh.hintedStringPos.first_pos.char_index),
                                      editor.GetCoordinate(search_end_pos));
    }
    if (hint.replace_till_cursor)
        csh.TrimHintedStringPos(start);

    for (EHintFilter filter_by_uc : { EHintFilter(Settings.filter_hints), EHintFilter::None }) {
        //Save hints
        std::vector<CshHint> saved_hints = csh.Hints;
        //Now process the list of hints: fill extra fields, compute size, filter by under_cursor and compact
        if (csh.Hints.size()) {
            cairo_surface_t *surf = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, HINT_GRAPHIC_SIZE_X, HINT_GRAPHIC_SIZE_Y);
            Canvas canvas(Canvas::PNG, surf, Block(0, HINT_GRAPHIC_SIZE_X, 0, HINT_GRAPHIC_SIZE_Y));
            csh.ProcessHints(canvas, nullptr, under_cursor, filter_by_uc, /*compact=*/true);
        }

        //See how many of the remaining hits fit the word under the cursor
        unsigned no_selectable = 0; //count how many selectable hints we have fow which 'text' is a prefix
        unsigned no_unselectable = 0; //count how many un-selectable hints we have
        hint.first_auto_match = -1;
        const CshHint *hit = nullptr; //The last selectable hint
        int second_auto_match = -1;
        for (const CshHint &h : csh.Hints)
            //find a selectable item or one that the text under cursor fits
            if (!h.selectable) {
                no_unselectable++;
            } else if (h.can_autoselect) {
                no_selectable++;
                hit = &h;
                if (hint.first_auto_match<0)
                    hint.first_auto_match = &h - csh.Hints.data();
                else
                    second_auto_match = &h - csh.Hints.data();
            }
        //if nothing under the cursor and we have multiple options to select
        //do not select any of them in the hint box
        if (under_cursor.empty() && second_auto_match>=0)
            hint.first_auto_match = -1;
        else if (csh.allow_anything) //Likewise, no auto-selection if the place allows free typing
            hint.first_auto_match = -1;
        //we will not consider un-selectable hints if we have something under the cursor
        if (under_cursor.length()) no_unselectable = 0;
        //Check if we have only one hint (group hints count as multiple)
        const bool onlyOne = no_unselectable==0 && no_selectable==1
                          && hit->decorated[hit->decorated.size()-1]!='*';
        if (onlyOne && hint.is_user_requested && !csh.allow_anything && hit->GetReplacementString() != under_cursor) {
            //If we are about to start hint mode due to a Ctrl+Space and there is only one selectable hit
            //then auto complete without popping up the hint list.
            HintSubstitute(hit);
            goto cancel;
        } else if (onlyOne && hit->GetReplacementString()== under_cursor) {
            //if there is only one hit and it is equal to the word under cursor, cancel hit mode,
            //but not if it was a Ctrl+Space - in that case show the only choice as a feedback
            //to Ctrl+Space
            if (hint.is_user_requested) {
                //...if the user requested the thing, preprocess again, but without
                //filtering.
                if (filter_by_uc!=EHintFilter::None) {
                    csh.Hints = saved_hints; //restore hints;
                    continue; //repeat processing with filter_by_uc=false
                }
                //if filtering was already off, just keep what we have now
            } else
                goto cancel;
        }
        break;
    }
    FillAttr white(ColorType(255, 255, 255));
    white.MakeComplete();
    constexpr double X = HINT_GRAPHIC_SIZE_X*5, Y = HINT_GRAPHIC_SIZE_Y*5;
    cairo_surface_t *surf = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, int(X), int(Y));
    Canvas canvas(Canvas::PNG, surf, Block(0, 0, X, Y));
    if (!canvas.cannot_draw())
        cairo_scale(canvas.GetContext(), X/HINT_GRAPHIC_SIZE_X, Y/HINT_GRAPHIC_SIZE_Y);
    StringFormat sf;
    for (const auto &h : csh.Hints) {
        HintData::ItemData &item = hint.items.emplace_back();
        if (h.callback) {
            canvas.Fill({0,0}, {HINT_GRAPHIC_SIZE_X, HINT_GRAPHIC_SIZE_Y}, white);
            h.callback(&canvas, h.param, csh);
            cairo_surface_flush(surf); //We still have a context for this surface in canvas. I hope it is OK.
            item.graphics = TextureFromSurface(surf);
        }
        sf.Default();
        sf.Apply(h.decorated); //h is const, so this matches the Apply(string_view) variant and will leave 'decorated' unchanged.
        item.bold = sf.bold.value_or(ETriState::no)==ETriState::yes;
        item.italics = sf.italics.value_or(ETriState::no)==ETriState::yes;
        item.underline= sf.underline.value_or(ETriState::no)==ETriState::yes;
        ColorType color = sf.color.value_or(ColorType(0, 0, 0)); //black
        item.color = IM_COL32(color.r, color.g, color.b, color.a);
        //Add * at the end if it ends in a dot.
        if (h.plain.ends_with('.')) {
            if (h.replaceto.empty()) h.replaceto = h.plain;
            h.plain.push_back('*');
        }
    }
    canvas.Fill({0,0}, {HINT_GRAPHIC_SIZE_X, HINT_GRAPHIC_SIZE_Y}, white);
    cairo_surface_flush(surf); //We still have a context for this surface in canvas. I hope it is OK.
    hint.empty_graphics = TextureFromSurface(surf);
    return csh.Hints.size()>0; //if we have no hints, cancel hint mode
}

void EditorChartData::HintCancel(bool force) {
    skip_next_space = hint.is_user_requested; //swallow the Space char on the Space keyup event after a Ctrl+Space
    hint.active = hint.restart_hint && !force;
    hint.restart_hint = false;
    hint.is_user_requested = false;
    hint.search_till_cursor = false;
    hint.replace_till_cursor = false;
}

void EditorChartData::HintSubstitute(const CshHint *h) {
    const TextEditor::Coordinates from = editor.GetCoordinate(hint.csh->hintedStringPos.first_pos.char_index);
    const TextEditor::Coordinates till = editor.GetCoordinate(hint.csh->hintedStringPos.end_pos.char_index); 
    editor.SetSelection(from, till);
    editor.DoNotMergeNextChangeForUndo();
    editor.Insert((h->replaceto.empty() ? h->plain : h->replaceto).c_str());
    editor.DoNotMergeNextChangeForUndo();
    hint.restart_hint = h->keep;
    hint.is_user_requested = false;
    hint.search_till_cursor = false;
    hint.replace_till_cursor = false;
    if (!h->keep) hint.active = false;
}


void ConfigData::UpdateWindowTitle() {
    window_title = (IsDirty() ? "*" : "") + file_string();
}

GUIReturn ConfigData::Ret(bool focus) {
    const bool do_exit = [this] {
        if (exit_requested) switch (CheckOverwrite("Unsaved changes to %s. Do you want to save now?")) {
        case StatusRet::Completed: return true;
        case StatusRet::Cancelled: exit_requested = false; return false;
        case StatusRet::Ongoing: return false;
        }
        return false;
    }();
    return {do_exit ? GUIReturn::Exit
            : presentation_mode ? GUIReturn::FullScreen
            : focus ? GUIReturn::Focus
            : GUIReturn::None, window_title };
}

//returns true if we added page selector
bool PageSelector() {
    if (!Config.C || !Config.C->pChart || Config.C->pChart->GetPageNum()<=1) return false;
    static int page_selected = 0;
    page_selected = (int)Config.C->page;
    ImGui::Text("Page:"); ImGui::SameLine();
    Help("Select which page of the chart to view. 0 shows all.");
    ImGui::SetNextItemWidth(80*Config.dpi_mul);
    if (ImGui::InputInt("##Page", &page_selected, 1, 1, ImGuiInputTextFlags_CharsDecimal)) {
        page_selected = page_selected<0 ? 0 : (unsigned)page_selected > Config.C->pChart->GetPageNum() ? (int)Config.C->pChart->GetPageNum() : page_selected;
        if ((int)Config.C->page!=page_selected) {
            Config.C->page = page_selected;
            Config.C->ResetCompileCache();
        }
    }
    Help("Select which page of the chart to view. 0 shows all.");
    return true;
}

void AddUnderLine() {
    ImVec2 min = ImGui::GetItemRectMin();
    ImVec2 max = ImGui::GetItemRectMax();
    ImColor col = ImGui::GetStyle().Colors[ImGuiCol_Text];
    min.y = max.y;
    ImGui::GetWindowDrawList()->AddLine(min, max, col, 1.0f);
}

std::array<bool, 3> GetShiftKeys() noexcept {
    ImGuiIO& io = ImGui::GetIO();
    const bool shift = io.KeyShift;
    const bool ctrl = io.ConfigMacOSXBehaviors ? io.KeySuper : io.KeyCtrl;
    const bool alt = io.ConfigMacOSXBehaviors ? io.KeyCtrl : io.KeyAlt;
    return {shift, ctrl, alt};
}

bool EditorChartData::HandleKeyboardShortcuts() {
    const auto [shift, ctrl, alt] = GetShiftKeys();
    auto& io = ImGui::GetIO();
    if (io.KeyCtrl && !io.KeyAlt && !shift && ImGui::IsKeyPressed(ImGuiKey_Space)) { //use the Ctrl key also on Apple
        HintStart();
        skip_next_space = true;
    } else if (escape_pressed) {
        if (hint.active) {
            HintCancel(true);
        } else if (Config.show_search) { //Drop search window
            Config.show_search = false;
        } else if (const auto s = editor.GetSelection(); s.first!=s.second) { //cancel selection
            editor.SetSelection(editor.GetCursorPosition(), editor.GetCursorPosition());
        } else if (Config.presentation_mode || tracking_mode) { //Kill both full screen and tracking mode
            Config.presentation_mode = false;
            tracking_mode = false;
            for (auto& o : overlays) o.do_fade_out = true;
        } else
            return false; //escape pressed on vanilla window - allow Example window to close
        shift_focus_to_editor = true; //If the user just pressed escape, we shift focus to the editor
    } else if (!ctrl && !alt && !shift && ImGui::IsKeyPressed(ImGuiKey_F4)) {
        ElementRef element = GetArc(editor.GetCursorPosition());
        if (element) Animate(element);
        // else beep
    } else if (!alt && ImGui::IsKeyPressed(ImGuiKey_F8)) {
        NavigateErrors(shift, ctrl);
    } else if (ctrl && !alt && !shift && ImGui::IsKeyPressed(ImGuiKey_R)) {
        do_open_rename = true;
    } else if (ctrl && !alt && !shift && ImGui::IsKeyPressed(ImGuiKey_F)) {
        Config.show_search = true;
        refocus_search_window = true;
    } else
        return false;
    return true;
}

void EditorChartData::DoEditor(const char* editor_name) {
    auto& io = ImGui::GetIO();

    if (Settings.instant_compile)
        CompileIfNeeded();

    if (shift_focus_to_editor) {
        ImGui::SetNextWindowFocus();
        shift_focus_to_editor = false;
    }
    if (skip_next_space && io.InputQueueCharacters.contains(' ')) {
        io.InputQueueCharacters.resize(0);
        skip_next_space = false;
    }
    ImGui::PushFont(GetFont(false, false, false));
    const auto undo_status_before = editor.GetUndoBufferIndex();
    editor.SetAlwaysRenderCursor(hint_window_shown);
    editor.Render(editor_name);
    PostEditorRender(undo_status_before);
    //Context menu
    if (ImGui::IsMouseReleased(ImGuiMouseButton_Right))
        ImGui::OpenPopupOnItemClick("context");
    static std::string ask_entity_name_replace;
    static Csh::EntityNameUnderResult entity_under_to_replace;
    static ImVec2 rename_dialog_location;
    ImGui::PushFont(GetFont(true, false, false));
    if (ImGui::BeginPopup("context")) {
        const TextEditor::Coordinates cursor_now = editor.ScreenPosToCoordinates(ImGui::GetWindowPos());
        ElementRef element = GetArc(cursor_now);
        if (ImGui::MenuItem("Highlight this element", "F4", false, bool(element)))
            Animate(element);
        const int pos = editor.GetCharPos(cursor_now);
        Csh::EntityNameUnderResult entity_under;
        if (hint.csh && pos>=0)
            entity_under = hint.csh->EntityNameUnder(pos+1); //we need a 1-indexed char pos
        const std::string menuname = entity_under
            ? StrCat("Rename ", lang->entityname, " '", entity_under.full_entity_name, "' (in selection)")
            : StrCat("Rename ", lang->entityname, " (in selection)");
        if (ImGui::MenuItem(menuname.c_str(), "Ctrl+R", false, (bool)entity_under) && hint.csh && lang) {
            entity_under_to_replace = entity_under; //this triggers the replace dialog
            rename_dialog_location = {ImGui::GetItemRectMin().x, ImGui::GetItemRectMin().y};
        }
        if (ImGui::MenuItem("Search and Replace...", "Ctrl+F")) {
            Config.show_search = true;
            refocus_search_window = true;
        }
        ImGui::EndPopup(); //ctx menu
    }
    if (lang) {
        if (hint.csh && do_open_rename) {
            const int pos = editor.GetCharPos(editor.GetCursorPosition());
            if (pos>=0) {
                entity_under_to_replace = hint.csh->EntityNameUnder(pos+1); //we need a 1-indexed char pos
                auto xywh = editor.CoordinatesToScreenPos(editor.GetCursorPosition());
                if (xywh)
                    rename_dialog_location = xywh->first + xywh->second;
                else
                    rename_dialog_location = {0,0};
            } //else beep or flash
        }
        static bool rename_is_open = false;
        if (entity_under_to_replace && !rename_is_open) {
            ImGui::OpenPopup(("Rename "+lang->entityname).c_str());
            ask_entity_name_replace = entity_under_to_replace.csh->AskReplace(entity_under_to_replace.full_entity_name, entity_under_to_replace.byte_pos);
            ask_entity_name_replace.reserve(100);
            rename_is_open = true;
        }
        if (rename_dialog_location.x && rename_dialog_location.y)
            ImGui::SetNextWindowPos(rename_dialog_location);
        //This is to avoid clipping the TextInput for one frame on appearing (prevents focus from working)
        //See https://github.com/ocornut/imgui/issues/4079
        ImGui::SetNextWindowSize(ImVec2{100,70}*Config.dpi_mul, ImGuiCond_Appearing);
        if (ImGui::BeginPopupModal(("Rename "+lang->entityname).c_str(), &rename_is_open,
                                   ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings)) {
            ImGui::TextUnformatted(StrCat("Enter new name for ", lang->entityname, " '", entity_under_to_replace.full_entity_name, "':").c_str());
            ImGui::PushFont(GetFont(false, false, false));
            ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1);
            if (ImGui::IsWindowAppearing())
                ImGui::SetKeyboardFocusHere();
            const bool text_trigger
                = ImGui::InputText("##renameinput", ask_entity_name_replace.data(), ask_entity_name_replace.capacity(),
                                   ImGuiInputTextFlags_AutoSelectAll |ImGuiInputTextFlags_EnterReturnsTrue);
            ImGui::PopStyleVar();
            ImGui::PopFont();
            const bool ok_trigger = ImGui::Button("OK") || ImGui::IsKeyPressed(ImGuiKey_Enter);
            if (text_trigger || ok_trigger) {
                if (hint.csh && ask_entity_name_replace.size()) {
                    const auto [start, end] = editor.GetSelectionCharPos(); //zero-based indices
                    if (start>=0 && end>=0) {
                        auto s = start + 1, e = end + 1; //1-based index expected by CSH
                        std::string new_text
                            = entity_under_to_replace.csh->ReplaceEntityName(
                                entity_under_to_replace.full_entity_name, entity_under_to_replace.byte_pos,
                                ask_entity_name_replace.data(), //'ask_entity_name_replace' is now a string buffer, where length does not match the C string inside its capacity.
                                s, e);
                        if (new_text.size() && new_text != entity_under_to_replace.csh->input_text) {
                            if (!entity_under_to_replace.pos_inline.empty())
                                editor.SetSelection(editor.GetCoordinate(entity_under_to_replace.pos_inline.first_pos.char_index),
                                                    editor.GetCoordinate(entity_under_to_replace.pos_inline.end_pos.char_index));
                            else
                                editor.SelectAll();
                            editor.Insert(new_text.c_str());
                            if (start == end)
                                editor.SetSelection(editor.GetCoordinate(start), editor.GetCoordinate(end));
                            else
                                editor.SetSelection(editor.GetCoordinate(start),
                                                    editor.GetCoordinate(end - UTF8len(entity_under_to_replace.csh->input_text) + UTF8len(new_text)));

                        }
                    }
                }
                ImGui::CloseCurrentPopup();
                rename_is_open = false;
                entity_under_to_replace = {};
            }
            ImGui::SameLine();
            if (ImGui::Button("Cancel") || escape_pressed) {
                ImGui::CloseCurrentPopup();
                rename_is_open = false;
                entity_under_to_replace = {};
                shift_focus_to_editor = true;
            }
            ImGui::EndPopup(); //ask rename dialog
        } else
            entity_under_to_replace = {}; //when we closed the dialog with the X button clear this to prevent re-open
    }
    ImGui::PopFont();

    constexpr float ratio = float(HINT_GRAPHIC_SIZE_X)/float(HINT_GRAPHIC_SIZE_Y);
    const float HintGraphicHeight = ImGui::GetFontSize()+2;
    const float HintGraphicWidth = HintGraphicHeight*ratio;
    if (hint.active) {
        if (hint.is_user_requested && hint.csh->Hints.size() == 1 && hint.csh->Hints.front().selectable && !hint.csh->allow_anything) {
            //User clicked Ctrl+Space and got one selectable hint -> substitute it right away and do not show the dialog
            HintSubstitute(&hint.csh->Hints.front());
        } else if (!hint_window_shown) {
            TextEditor::Coordinates hint_at = editor.GetCursorPosition();
            const int hint_word_char_off = hint.csh->cursor_pos.char_index - hint.csh->hintedStringPos.first_pos.char_index; //Which character in the hinted string (word under cursor) are the cursor at
            _ASSERT(hint_word_char_off>=0);
            _ASSERT(hint_at.mColumn>=hint_word_char_off);
            hint_at.mColumn = std::max(0, hint_at.mColumn-std::max(0, hint_word_char_off));
            if (auto xywh = editor.CoordinatesToScreenPos(hint_at)) {
                //Check how much space is below the line for the hint window
                ImVec2 at(xywh->first.x - HintGraphicWidth*Settings.editor_font_scale - 2, //-2 is window border and whatnot
                          xywh->first.y + xywh->second.y);
                const float max_y = ImGui::GetMainViewport()->WorkPos.x + ImGui::GetMainViewport()->WorkSize.y;
                const float min_height = editor.mFontSize*3;
                if (at.y+min_height < max_y) {
                    max_hint_height = std::min(200*Settings.editor_font_scale, max_y - at.y);
                } else {
                    max_hint_height = std::min(200*Settings.editor_font_scale,
                                          hint.csh->Hints.size()*editor.mFontSize);
                    at.y = xywh->first.y - max_hint_height;
                }
                ImGui::SetNextWindowPos(at, ImGuiCond_Appearing);
                ImGui::OpenPopup("Hint");
                hint_window_shown = true;
                described_hint = highlighted_hint = selected_hint = hint.first_auto_match;
            }
        } else if ((ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_Tab))
                   && (0<=selected_hint && selected_hint<(int)hint.csh->Hints.size())) {
            auto h = std::next(hint.csh->Hints.begin(), selected_hint);
            if (h->selectable)
                if (h != hint.csh->Hints.end())
                    HintSubstitute(&*h);
        } else if (auto h = std::ranges::find_if(hint.csh->Hints, [](const CshHint& h) {return h.selectable && h.can_autoselect; })
                   ; ImGui::IsKeyPressed(ImGuiKey_Tab) && h != hint.csh->Hints.end()) {
            HintSubstitute(&*h);
        } else {
            if (!io.InputQueueCharacters.empty()) {
                bool need_recolor = false;
                const auto index_before = editor.GetUndoBufferIndex();
                for (int i = 0; i < io.InputQueueCharacters.Size; i++) {
                    auto c = io.InputQueueCharacters[i];
                    const bool non_hint_char = !isalnum(c) && c != '_' && c>=32 && c<127; //non-alphanumeric: substitute hint first
                    if (non_hint_char && !hint.csh->hadEscapeHint) { //for escape hints we never substitute on a typed char
                        if (need_recolor) {
                            editor.ReColor();
                            selected_hint = -1;
                            need_recolor = false;
                        }
                        if (0<=selected_hint && selected_hint<(int)hint.csh->Hints.size()) {
                            const auto h = std::next(hint.csh->Hints.begin(), selected_hint);
                            if (hint.active && h!=hint.csh->Hints.end() && h->selectable && !hint.csh->allow_anything) {
                                HintSubstitute(&*h);
                                if (h->GetReplacementString().back()=='.'&& c=='.')
                                    continue; //hint ends with dot, user pressed dot - swallow the dot
                            } else
                                HintCancel(true);
                        }
                    }
                    if (c >= 32)
                        editor.EnterCharacter(c, io.KeyShift);
                    else //we ignore space as it may remain with us from Ctrl+Space
                        continue;
                    //we have changed the text
                    hint_window_shown = false; //Changing the text will re-focus the editor and kill the popup - and we want to keep it
                    hint.is_user_requested = false; //From this on, if a single match remains, we do not automatically substitute it.
                    need_recolor = true;
                }
                io.InputQueueCharacters.resize(0);
                PostEditorRender(index_before);
            }
            //Any key other than this needs to be handled by the editor
            const static std::array hint_window_keys = {ImGuiKey_UpArrow, ImGuiKey_DownArrow, ImGuiKey_PageUp, ImGuiKey_PageDown, ImGuiKey_Enter, ImGuiKey_Tab};
            if (std::ranges::none_of(hint_window_keys, [](auto k) { return ImGui::IsKeyPressed(k); })) {
                const auto index_before = editor.GetUndoBufferIndex();
                editor.HandleKeyboardInputs(false);
                PostEditorRender(index_before);
                if (editor.GetUndoBufferIndex()!=index_before)
                    hint_window_shown = false; //Changing the text will re-focus the editor and kill the popup - and we want to keep it
                hint.is_user_requested = false; //From this on, if a single match remains, we do not automatically substitute it.
            }
        }
    } else
        hint_window_shown = false;
    if (hint.active) {
        ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 2, 2 });
        ImGui::SetNextWindowSizeConstraints(ImVec2{ 0, 0 }, ImVec2(200 * Settings.editor_font_scale, max_hint_height));
        if (ImGui::BeginPopup("Hint", ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse
                                | ImGuiWindowFlags_NoSavedSettings)) {
            ImGui::SetWindowFontScale(Settings.editor_font_scale);
            int i = 0;
            int sel = selected_hint, hoo = -1; //the index of the item selected & hoovered
            ImVec2 sxy, hxy;        //the orign of the above items.
            ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, { 0, 2 });
            if (selected_hint < 0) {
                ImGui::SetItemDefaultFocus();
                ImGui::Dummy(ImVec2(0, 0)); //No automatic selection of first item
            }
            ImDrawList* const draw_list = ImGui::GetWindowDrawList();
            for (const CshHint& h : hint.csh->Hints) {
                const ImVec2 origin = ImGui::GetCursorScreenPos();
                if (hint.empty_graphics) {
                    ImGui::Image(hint.items[i].graphics ? *hint.items[i].graphics : *hint.empty_graphics,
                                    ImVec2{ HintGraphicWidth, HintGraphicHeight }*Settings.editor_font_scale);
                    ImGui::SameLine();
                }
                const ImVec2 origin2 = ImGui::GetCursorScreenPos();
                ImGui::PushStyleColor(ImGuiCol_Text, hint.items[i].color);
                ImGui::PushFont(GetFont(false, hint.items[i].bold, hint.items[i].italics));
                const ImVec2 size = ImGui::CalcTextSize(h.plain.c_str());
                if (h.highlight.first < h.highlight.second) {
                    const float from = ImGui::CalcTextSize(h.plain.c_str(), h.plain.c_str() + h.highlight.first).x;
                    const float till = ImGui::CalcTextSize(h.plain.c_str(), h.plain.c_str() + h.highlight.second).x;
                    draw_list->AddRectFilled(origin2 + ImVec2(from, 0), origin2 + ImVec2(till, size.y), IM_COL32(224, 224, 255, 255));
                }
                if (i == selected_hint)
                    ImGui::SetItemDefaultFocus();
                if (ImGui::Selectable(h.plain.c_str(), i == selected_hint, ImGuiSelectableFlags_DontClosePopups, size)
                    && h.selectable && hint.active)
                    HintSubstitute(&h);
                if (hint.items[i].underline)
                    AddUnderLine();
                ImGui::PopFont();
                ImGui::PopStyleColor();
                if (ImGui::IsItemFocused()) sel = i;
                if (sel == i) sxy = origin;
                if (ImGui::IsItemHovered()) hoo = i;
                if (hoo == i) hxy = origin;
                i++;
            }
            ImGui::PopStyleVar();
            if (sel >= 0 && sel != selected_hint)
                selected_hint = described_hint = sel;
            else if (highlighted_hint != hoo) {
                highlighted_hint = hoo;
                if (hoo >= 0) described_hint = hoo;
                else described_hint = sel; //no item hoovered over
            }
            if (0 <= described_hint && described_hint < (int)hint.csh->Hints.size()) {
                const std::string& descr = std::next(hint.csh->Hints.begin(), described_hint)->description;
                const HintData::ItemData& item = hint.items[described_hint];
                if (descr.size()) {
                    if (described_hint == selected_hint)
                        ImGui::SetNextWindowPos({ sxy.x + ImGui::GetWindowWidth(), sxy.y });
                    else if (described_hint == highlighted_hint)
                        ImGui::SetNextWindowPos({ hxy.x + ImGui::GetWindowWidth(), hxy.y });
                    else {
                        _ASSERT(0);
                    }
                    ImGui::PushFont(GetFont(true, false, false));
                    ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4);
                    ImGui::BeginTooltip();
                    const float width = ImGui::GetFontSize() * 35.0f;
                    const float height = ImGui::CalcTextSize(descr.c_str(), nullptr, false, width).y;
                    float Y = 0;
                    if (item.graphics) {
                        constexpr float ratio = float(HINT_GRAPHIC_SIZE_X) / float(HINT_GRAPHIC_SIZE_Y);
                        Y = ImGui::GetFontSize() * 2;
                        ImGui::Image(*item.graphics, { Y * ratio, Y });
                        ImGui::SameLine();
                    }
                    if (Y && Y > height) {
                        ImGui::BeginGroup();
                        ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, { 0, 0 });
                        ImGui::Dummy({ 1, (Y - height) / 2 });
                    }
                    ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + width);
                    ImGui::TextUnformatted(descr.c_str());
                    ImGui::PopTextWrapPos();
                    if (Y && Y > height) {
                        ImGui::PopStyleVar();
                        ImGui::EndGroup();
                    }
                    ImGui::EndTooltip();
                    ImGui::PopStyleVar();
                    ImGui::PopFont();
                }
            }
            ImGui::EndPopup();
        } else {
            HintCancel(false);
            hint_window_shown = false;
        }
        ImGui::PopStyleVar();
    }
    ImGui::PopFont();
}

//M tells which part of the rectangle is at O.
//-(0,0) upper left corner, (0.5, 0.5) its center, (1,0) upper right corner
ImVec2 ShowText(const char* text, ImVec2 O, ImVec2 M, ImU32 color, float fade, float font_size=12) {
    const float fontsize = font_size*Config.dpi_mul;
    const ImFont* font = GetFont(true, true, false);
    const ImVec2 fsize = font->CalcTextSizeA(fontsize, 1e10, 0, text);
    const ImVec2 off = { fontsize/2, fontsize / 4 };
    const ImVec2 size = fsize+off*2;
    const unsigned char A = (unsigned char)(128*fade);
    O.x -= M.x * size.x;
    O.y -= M.y * size.y;
    color = (color & ~IM_COL32_A_MASK) | ((ImU32)(A)<<IM_COL32_A_SHIFT);
    ImDrawList* const draw_list = ImGui::GetWindowDrawList();
    draw_list->AddRectFilled(O, O + fsize + off * 2, color , fontsize / 3);
    draw_list->AddText(font, fontsize, O+off, IM_COL32(255, 255, 255, (unsigned char)(255*fade)), text);
    return size;
}

std::pair<ImVec2, ImVec2>
EditorChartData::DoChart(bool show_errors, bool fit_to_window, bool fit_to_width,
                         bool allow_track_change) {
    if (!compile_cache.chart)
        DrawCache();

    //Calculate how much space for the error window calculate the animation status
    const size_t errnum = pChart ? pChart->Error.GetErrorNum(Settings.warnings, Settings.technical_info) : 0;
    const ImVec2 save_cursor = ImGui::GetCursorPos();
    const float line_height = ImGui::GetTextLineHeight()*1.1f;
    const float extra_height = 25; //Add this to the window to cater for vertical scrollbar, padding and border
    ImVec2 canvas_sz = ImGui::GetContentRegionAvail();   // Resize chart graphics to what's available
    const float target_space_for_errors = errnum
        //Occupy at most 3/4 of the drawing space for errors
        ? between(line_height*errnum+extra_height, canvas_sz.y*0.1f, canvas_sz.y*0.75f)
        : 0;
    space_for_errors = anim_move_value(space_for_errors, target_space_for_errors);

    //show error window
    if (space_for_errors && show_errors) {
        ImGui::SetCursorPosY(save_cursor.y + canvas_sz.y - space_for_errors);
        canvas_sz.y -= space_for_errors + 1; //+1 to prevent overwriting the edge of the error window
        ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32_WHITE);
        ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {0,0});
        ImGui::BeginChild("errors", ImVec2(0, target_space_for_errors), true, ImGuiWindowFlags_HorizontalScrollbar);
        int now_selected = selected_error;
        for (size_t i = 0; i<errnum; i++) {
            const ErrorElement* E = pChart->Error.GetError(i, Settings.warnings, Settings.technical_info);
            const bool was_selected = selected_error==int(i);
            const ImU32 color = [E] {
                switch (E->line_type) {
                case ErrorElement::ELineType::Context:       return IM_COL32(128, 128, 128, 255);
                case ErrorElement::ELineType::Substitutuion: return IM_COL32(192, 192, 192, 255);
                case ErrorElement::ELineType::Main: switch (E->type) {
                case ErrorElement::EType::Error:             return IM_COL32(128, 0, 0, 255);
                case ErrorElement::EType::Warning:           return IM_COL32(0, 128, 0, 255);
                case ErrorElement::EType::TechnicalInfo:     return IM_COL32(0, 64, 128, 255);
                }
                }
                return IM_COL32_BLACK;
            }();
            ImGui::PushStyleColor(ImGuiCol_Text, was_selected ? IM_COL32_WHITE : color);
            ImGui::PushStyleColor(ImGuiCol_HeaderActive, color);
            ImGui::PushStyleColor(ImGuiCol_Header, color);
            ImColor c(color);
            if (!was_selected) //dim the float color if not selected
                for (float* f :{&c.Value.x, &c.Value.y, &c.Value.z, &c.Value.w})
                    *f = 0.7f * 0.3f**f;
            ImGui::PushStyleColor(ImGuiCol_HeaderHovered, c.Value);
            constexpr size_t ImGuiSelectableFlags_SelectOnClick = 1 << 22;  // COPIED FROM IMGUI INTERNAL!!! Override button behavior to react on Click (default is Click+Release)
            //remove filename of the currently edited file for shorter error lines
            //but not if the filename is missing and we already start with a number
            const char* show_text = E->relevant_line.file != main_file_no || E->text.empty() || ('0' <= E->text.front() && E->text.front() <= '9')
                ? E->text.c_str()
                : E->text.c_str() + (E->text.find(':')+1); //rely on npos == -1
            static_assert(std::string::npos+1 == 0);
            if (ImGui::Selectable(show_text, was_selected, ImGuiSelectableFlags_SelectOnClick, ImVec2{0, line_height}))
                now_selected = int(i);
            ImGui::PopStyleColor(4);
        }
        if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows))
            shift_focus_to_editor = true;
        ImGui::EndChild();
        ImGui::PopStyleVar();
        ImGui::PopStyleColor();

        //If selection changed, jump to position in file
        if (now_selected!=selected_error) {
            selected_error = now_selected;
            GotoError();
        }
        ImGui::SetCursorPos(save_cursor); //Draw the picture above the errors
    }

    const ImVec2 full_canvas_sz = canvas_sz;
    if (fit_to_window) FitToWindow(full_canvas_sz);
    if (fit_to_width) FitToWidth(full_canvas_sz);

    //calculate compilation status
    constexpr float fade_ms = 500;
    compile_fade = anim_time_value(compile_fade, float(compile_data.is_compiling()), 1000.f/fade_ms);
    const unsigned char fade_int = (unsigned char)(round(compile_fade*192));

    //draw the chart
    const ImVec2 image_viewport_origin = ImGui::GetCursorPos();
    const ImVec2 window_offset = ImGui::GetWindowPos();
    ImDrawList* const draw_list = ImGui::GetWindowDrawList();
    if (compile_cache.chart && full_canvas_sz.x>0 && full_canvas_sz.y>0) {
        auto& io = ImGui::GetIO();
        const ImVec2& total = compile_cache.chart->size;
        const ImVec2 offset = max(ImVec2(0, 0), canvas_sz - total)/2; //nonzero, if the top-left corner of the image is not at (0,0) (center chart when small)
        canvas_sz = min(canvas_sz, total);
        scrolling = between(scrolling, {0,0}, max({0,0}, total-canvas_sz));
        ImVec2 uv1{scrolling.x/total.x, scrolling.y/total.y}, uv2{uv1.x+canvas_sz.x/total.x, uv1.y+canvas_sz.y/total.y};
        ImGui::SetCursorPos(image_viewport_origin+offset);
        ImGui::Image(*compile_cache.chart->texture, canvas_sz, uv1, uv2);
        for (auto& o : overlays)
            if (o.texture && o.fade) {
                ImGui::SetCursorPos(image_viewport_origin+offset+o.offset-scrolling);
                ImGui::Image(*o.texture->texture, o.texture->size, {0,0}, {1,1}, {1, 1, 1, o.fade});
                if (o.do_fade_out) o.fade = anim_time_value(o.fade, 0);
            }
        //Overlays that faded out are handled in PruneAnimations();
        for (auto& ctrl : controls)
            if (ctrl.element && ctrl.size) {
                if (ctrl.element.inline_data) {
                    //Draw the controls of inlined charts. They may be rotated, so we do not use the cached textures
                    const Block& ctrl_loc_in_inline = ctrl.element.element->GetControlLocation();
                    //bb is the bounding box of the controls in the root chart's space
                    const Block bb = Contour(ctrl_loc_in_inline).Transform(ctrl.element.inline_data->inlined_to_parent).GetBoundingBox();
                    cairo_surface_t* surf = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, int(bb.x.Spans()*zoom), int(bb.y.Spans()*zoom));
                    cairo_status_t status = cairo_surface_status(surf);
                    if (status != CAIRO_STATUS_SUCCESS) {
                        cairo_surface_destroy(surf);
                        continue;
                    }
                    cairo_t* cr = cairo_create(surf);
                    cairo_scale(cr, zoom, zoom);
                    cairo_translate(cr, -bb.x.from, -bb.y.from);
                    const TRMatrix M = ctrl.element.inline_data->inlined_to_parent;
                    const cairo_matrix_t m{ .xx = M.a1.x, .yx = M.ab.y, .xy = M.ab.x, .yy = M.a1.y, .x0 = M.a0.x, .y0 = M.a0.y };
                    cairo_transform(cr, &m);
                    cairo_translate(cr, ctrl_loc_in_inline.x.from, ctrl_loc_in_inline.y.from);
                    for (EGUIControlType t : ctrl.element.element->GetControls()) {
                        Element::DrawControl(cr, t, ctrl.size);
                        cairo_translate(cr, 0, Element::control_size.y);
                    }
                    cairo_destroy(cr);
                    if (TextureID texture = TextureFromSurface(surf)) {
                        ImGui::SetCursorPos(image_viewport_origin + offset + ImVec2(bb.UpperLeft()*zoom) - scrolling);
                        ImGui::Image(*texture, ImVec2(bb.Spans()*zoom), {0,0}, {1,1});
                    }
                    cairo_surface_destroy(surf);
                } else {
                    ImVec2 off(ctrl.element.element->GetControlLocation().UpperLeft() * zoom);
                    const ImVec2 size_off(Element::control_size * (zoom * (1 - ctrl.size) / 2));
                    for (EGUIControlType t : ctrl.element.element->GetControls()) {
                        if (const auto& texture = compile_cache.controls[int(t)]) {
                            ImGui::SetCursorPos(image_viewport_origin + offset + off - scrolling + size_off);
                            ImGui::Image(*texture->texture, texture->size * ctrl.size, { 0,0 }, { 1,1 });
                        }
                        off.y += float(Element::control_size.y) * zoom;
                    }
                }
                ctrl.size = anim_time_value(ctrl.size, 0, 5);
            }
        //remove controls that have disappeared
        std::erase_if(controls, [](const EditorChartData::Control& c) {return c.size==0.; });
        //Tackle autoheading
        const float heading = scrolling.y || heading_fade ? GetHeadingSize() : 0;
        if (heading) {
            ImGui::SetCursorPos(image_viewport_origin+offset);
            if (scrolling.y) {
                ImVec2 uv1{scrolling.x/total.x, 0}, uv2{uv1.x+canvas_sz.x/total.x, heading/total.y};
                ImGui::Image(*compile_cache.chart->texture, ImVec2{canvas_sz.x, heading}, uv1, uv2);
            }
            draw_list->AddRectFilled(window_offset+image_viewport_origin+offset,
                                     window_offset+image_viewport_origin+offset+ImVec2(canvas_sz.x, heading),
                                     IM_COL32(0, 0, 0, 30*heading_fade));
            if (scrolling.y)
                draw_list->AddLine(window_offset+image_viewport_origin+ImVec2(offset.x, heading),
                                   window_offset+image_viewport_origin+ImVec2(offset.x+canvas_sz.x, heading),
                                   IM_COL32(0, 0, 0, 255*heading_fade), 1);
            heading_fade = anim_time_value(heading_fade, scrolling.y ? 1.f : 0.f, 3.f);
        }
        if (!fade_int) {
            //We are not compiling - draw tracking mode indicator if applicable
            tracking_mode_fade = anim_time_value(tracking_mode_fade, float(tracking_mode), 3);
            if (tracking_mode_fade) {
                const float margin = 12*Config.dpi_mul;
                const ImVec2 O{ image_viewport_origin.x + full_canvas_sz.x - margin, image_viewport_origin.y + margin };
                ShowText("Tracking mode", O, {1,0}, IM_COL32(255, 0, 0, 0), tracking_mode_fade);
            }
            //Get mouse input
            ImGui::SetCursorPos(image_viewport_origin);
            ImVec2 canvas_p0 = ImGui::GetCursorScreenPos();      // ImDrawList API uses screen coordinates!
            // Using InvisibleButton() as a convenience 1) it will advance the layout cursor and 2) allows us to use IsItemHovered()/IsItemActive()
            ImGui::InvisibleButton("canvas", full_canvas_sz, ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight);
            // Shift focus back to the editor
            if (ImGui::IsItemFocused()) shift_focus_to_editor = true;
            // Pan (we use a zero mouse threshold as the context menu is on the other button)
            // You may decide to make that threshold dynamic based on whether the mouse is hovering something etc.
            if (ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left, 0.f))
                scrolling = scrolling - io.MouseDelta; //'scrolling' will be normalized at the next frame above
            // Zoom & Mouse scroll
            if (ImGui::IsItemHovered() && io.MouseWheel) {
                if (io.KeyCtrl) {
                    const float new_zoom = between(float(zoom * pow(1.1f, between(io.MouseWheel, -5.f, +5.f))), min_zoom, max_zoom);
                    if (new_zoom != zoom) {
                        const ImVec2 P = io.MousePos - canvas_p0 + scrolling - offset; //mouse pos on the surface
                        const ImVec2 pivot{between(P.x/total.x, 0.f, 1.f), between(P.y/total.y, 0.f, 1.f)}; //mouse pos mapped to [0,1];[0,1] rectangle
                        const ImVec2 new_total = total/zoom*new_zoom;
                        const ImVec2 Q{new_total.x*pivot.x, new_total.y*pivot.y}; //mouse pos on new surface
                        scrolling = Q - io.MousePos + canvas_p0 + offset; //'scrolling' will be normalized at the next frame
                        SetZoom(new_zoom);
                    }
                } else {
                    scrolling.y -= between(io.MouseWheel, -5.f, +5.f)*40;
                }
            }
            //Tracking
            const bool chart_hoovered = ImGui::IsItemHovered() && !search_hoovered;
            bool clicked = ImGui::IsMouseReleased(ImGuiMouseButton_Left) && !ImGui::GetMouseDragDelta(ImGuiMouseButton_Left) && chart_hoovered;
            //if we are hoovering over some element controls, keep them live & check for clicks
            if (auto M = MouseToChart(io.MousePos - canvas_p0 + scrolling - offset)) {
                for (const EditorChartData::Control& ctrl : controls)
                    if (ctrl.element) {
                        const XY M_to_use = ctrl.element.inline_data ? M->Apply(ctrl.element.inline_data->inlined_to_parent.Invert()) : *M;
                        if (const EGUIControlType t = ctrl.element.element->WhichControl(M_to_use); t != EGUIControlType::INVALID) {
                            ShowControls(ctrl.element);
                            if (clicked)
                                ControlClicked(ctrl.element, t);
                            clicked = false;
                            break;
                        }
                    }
                const auto i = std::ranges::find_if(pChart->ismapData, [&M](const ISMapElement& l) {return l.rect.IsWithinBool(*M); });
                if (i != pChart->ismapData.end()) {
                    const auto [msg, clickable] = EvaluateReferenceForGUI(i->target);
                    ImGui::SetMouseCursor(clickable ? ImGuiMouseCursor_Hand : ImGuiMouseCursor_Arrow);
                    Help(StrCat(msg, '\n', i->target).c_str());
                    const auto [shift, ctrl, alt] = GetShiftKeys();
                    if (clickable && clicked && ctrl) {
                        ShowLink(i->target);
                        clicked = false;
                    }
                } else
                    ImGui::SetMouseCursor(ImGuiMouseCursor_Arrow);
            }

            const bool to_track = (tracking_mode || Settings.show_controls) && chart_hoovered;
            const ImVec2 M = [&]() { //determine where the mouse is in chart space
                ImVec2 M = io.MousePos - canvas_p0; //where mouse is relative to chart display area begin
                if (heading==0 || scrolling.y==0 ||  heading < M.y) //if we are not in the heading...
                    M = M + scrolling - offset;                     //... adjust this to be relative to chart origin (still multiplied by zoom)
                else
                    M.x += scrolling.x - offset.x;                  //... else adjust only the X dir - as the heading is shown
                return M;
            }();
            const ElementRef element_hoovered = clicked || to_track ? GetArc(M) : ElementRef{}; //save the search
            //Handle double clicks only if no Load/Save dialogs present and the mouse is over the chart
            if (chart_hoovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
                if (element_hoovered && element_hoovered.element->GetControls().size())  //Collapsible element double clicked
                    ControlClicked(element_hoovered, element_hoovered.element->GetControls().front());
                else if (allow_track_change) {
                    tracking_mode = !tracking_mode;
                    if (!tracking_mode)
                        for (auto& o : overlays) o.do_fade_out = true;
                }
            }
            if (element_hoovered) {
                if (tracking_mode) {
                    if (EditorChartData::Overlay* o = GetAnimation(element_hoovered.element); o && !o->do_fade_out)
                        o->do_fade_out = clicked;
                    else {
                        if (o) {
                            o->fade = clicked ? 1.f : std::max(0.4f, o->fade);
                            o->do_fade_out = !clicked;
                        } else
                            Animate(element_hoovered, clicked ? 1.f : 0.4f, !clicked);
                        if (cursor_saved_for != element_hoovered.element
                            && element_hoovered.element->file_pos.start.file == main_file_no
                            && element_hoovered.element->file_pos.end.file == main_file_no) {
                            if (cursor_saved_for == nullptr)
                                saved_cursor_pos = editor.GetCursorPosition();
                            cursor_saved_for = element_hoovered.element;
                            SetCursorTo(element_hoovered);
                        }
                    }
                } else {
                    if (Settings.show_controls && element_hoovered.element->GetControls().size())
                        ShowControls(element_hoovered);
                    if (clicked) {
                        Animate(element_hoovered);
                        SetCursorTo(element_hoovered);
                    }
                }
            } else if (cursor_saved_for) {
                cursor_saved_for = nullptr;
                editor.SetCursorPosition(saved_cursor_pos);
                editor.SetSelection(saved_cursor_pos, saved_cursor_pos);
            }
            if (tracking_mode && editor.IsCursorPositionChanged() && pChart) { //shall we disable it on dirty?
                TextEditor::Coordinates cursor_now = editor.GetCursorPosition();
                FileLineCol linenum(int(pChart->Error.Files.size()-1), cursor_now.mLine+1, cursor_now.mColumn+1);
                if (auto highlight_now = pChart->FindElement(linenum))
                    Animate(highlight_now, 0.4f, false);
                }

        }
    }
    if (fade_int) { //We are compiling - draw compile indicators, even if no chart yet
        if (Settings.instant_compile) {
            //smaller indication for instant compile
            constexpr float rad_speed_per_sec = 30 * float(M_PI/180);
            const float radius = 20*Config.dpi_mul;
            R1 += rad_speed_per_sec * sec_since_last_frame(); if (R1>2*M_PI) R1 -= float(2*M_PI);
            const ImVec2 C = window_offset + image_viewport_origin
                + ImVec2{full_canvas_sz.x - radius*1.1f, radius*1.1f}; //center of the circle
            constexpr int num_segments = 5;
            const float a_max = float(M_PI * 2.0f) * ((float)num_segments - 1.0f) / (float)num_segments;
            draw_list->PathArcTo(C, radius, R1, R1+a_max, num_segments - 1);
            draw_list->PathFillConvex(IM_COL32(192, 192, 192, fade_int));
            const ImFont* font = GetFont(true, false, false);
            constexpr const char* text = "Work";
            const float fontsize = 12*Config.dpi_mul;
            const ImVec2 fsize = font->CalcTextSizeA(fontsize, 1e10, 0, text);
            draw_list->AddText(font, fontsize, {C.x-fsize.x/2, C.y-fsize.y/2}, IM_COL32_WHITE, text);
        } else {
            //gray out and progress bar, when instant compilation is off
            draw_list->AddRectFilled(window_offset+image_viewport_origin,
                                     window_offset+image_viewport_origin+full_canvas_sz,
                                     IM_COL32(192, 192, 192, fade_int));
            if (float f = compile_data.get_progress(); f>=0 && compile_fade==1) {
                ImGui::SetNextWindowPos(image_viewport_origin+full_canvas_sz/2, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
                ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5);
                ImGui::Begin("ProgressBar", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings);
                ImGui::ProgressBar(f/100, {-1, -1}, "");
                ImGui::SetWindowSize({full_canvas_sz.x*0.8f, 20.f}, ImGuiCond_Always);
                ImGui::End();
                ImGui::PopStyleVar();
            }
        }
    }
    constexpr float message_fontsize = 15;
    const float X  = image_viewport_origin.x + message_fontsize*Config.dpi_mul;
    float Y = image_viewport_origin.y + message_fontsize*Config.dpi_mul;
    for (ConfigData::Message& m : Config.messages)
        if ((m.fade = anim_time_value(m.fade, m.target, 3))==m.target) m.target = 0;
    std::erase_if(Config.messages, [](const ConfigData::Message& m) { return m.fade==0; });
    for (ConfigData::Message& m : Config.messages) {
        if (m.Y<0) m.Y = Y;
        const ImVec2 size = ShowText(m.message.c_str(), { X, m.Y }, { 0, 0 }, IM_COL32(0, 128, 0, 0), std::min(m.fade, 1.f), message_fontsize);
        Y = m.Y + size.y*1.2f;
    }
    return {image_viewport_origin, full_canvas_sz};
}

void EditorChartData::DoSearchReplace(const ImVec2& image_viewport_origin, const ImVec2& full_canvas_sz) {
    static float search_fade = 0;
    search_fade = anim_time_value(search_fade, (float)Config.show_search, 3.f);
    static bool search_open = false;
    search_open = search_fade;
    if (search_fade) {
        ImGui::PushStyleVar(ImGuiStyleVar_Alpha, search_fade);
        ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1);
        ImVec2 pos = image_viewport_origin;
        pos.y += full_canvas_sz.y/2;
        ImGui::SetNextWindowPos(pos, ImGuiCond_Once, ImVec2{0, 0.5f});
        ImGui::SetNextWindowSize(ImVec2{100,70}*Config.dpi_mul, ImGuiCond_Appearing);
        if (refocus_search_window)
            ImGui::SetNextWindowFocus();
        refocus_search_window = false;
        ImGui::Begin("Search and Replace", &search_open,
                     ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse
                     | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings
                     | ImGuiWindowFlags_AlwaysAutoResize |ImGuiWindowFlags_NoResize);
        search_hoovered = ImGui::IsWindowHovered();
        static bool previous_frame_focus = false;
        static std::optional<std::pair<TextEditor::Coordinates, TextEditor::Coordinates>> saved_selection, last_found_pos;
        static std::string last_found_str;
        if (ImGui::IsWindowFocused() != previous_frame_focus && !previous_frame_focus) {
            //newly opened/focused search/replace
            saved_selection = editor.GetSelection();
            if (saved_selection->first>=saved_selection->second)
                saved_selection.reset();
            last_found_pos.reset();
        }
        previous_frame_focus = ImGui::IsWindowFocused();
        const auto [shift, ctrl, alt] = GetShiftKeys();
        const bool enter_pressed = ImGui::IsWindowFocused() && !shift && !ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_Enter);
        const bool F3_pressed = ImGui::IsWindowFocused() && !shift && !ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_F3);
        const bool Shift_F3_pressed = ImGui::IsWindowFocused() && shift && !ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_F3);
        const bool Alt_A_pressed = ImGui::IsWindowFocused() && !shift && !ctrl && alt && ImGui::IsKeyPressed(ImGuiKey_A);
        ImGui::Text("Search");
        static char search_buff[512] = "\0";
        if (ImGui::IsWindowAppearing())
            ImGui::SetKeyboardFocusHere();
        ImGui::InputText("##Search", search_buff, sizeof(search_buff), ImGuiInputTextFlags_AutoSelectAll);
        const bool search_pressed = ImGui::IsItemFocused() && enter_pressed;
        static bool case_sensitive = false;
        static bool whole_words_only = false;
        static bool in_selection_only = true;
        ImGui::Checkbox("Case Sensitive", &case_sensitive);
        ImGui::SameLine();
        ImGui::Checkbox("Whole Words Only", &whole_words_only);
        ImGui::PopStyleVar();
        const bool search_fw_pressed = ImGui::Button("Find Next") || F3_pressed;
        Help("Find the next occurrence (F3)");
        ImGui::SameLine();
        const bool search_bw_pressed = ImGui::Button("Find Prev") || Shift_F3_pressed;
        Help("Find the previous occurrence (Shift+F3)");
        ImGui::Dummy({10,10});
        ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1);
        ImGui::Text("Replace to");
        static char replace_buff[512] = "\0";
        ImGui::InputText("##Replace", replace_buff, sizeof(replace_buff), ImGuiInputTextFlags_AutoSelectAll);
        const bool replace_pressed = ImGui::IsItemFocused() && enter_pressed;
        ImGui::BeginDisabled(!saved_selection);
        ImGui::Checkbox("In Selection only", &in_selection_only);
        ImGui::EndDisabled();
        ImGui::PopStyleVar();
        const bool replace_once_pressed = ImGui::Button("Replace Next");
        Help("Replace one occurrence of the search string and moves to the next. If a selection is made and "
             "'In Selection only' is set, we limit the replaces to the selection");
        ImGui::SameLine();
        const bool replace_all_pressed = ImGui::Button("Replace All") || Alt_A_pressed;
        Help("Replace of all occurrence of the search string. If a selection is made and "
             "'In Selection only' is set, we limit the replaces to the selection. (Alt+A)");
        ImGui::End();
        ImGui::PopStyleVar();

        if (search_buff != last_found_str) {
            last_found_str.clear();
            last_found_pos.reset();
        }
        const bool last_found_selected = last_found_pos == editor.GetSelection();
        //OK, now for the search-replace logic
        if (search_pressed || search_fw_pressed || search_bw_pressed) {
        find_next:
            if (editor.Find(search_buff, case_sensitive, whole_words_only,
                            !search_bw_pressed, //will be true when doing this after a replace
                            {}, last_found_selected)) {
                last_found_pos = editor.GetSelection();
                last_found_str = search_buff;
            }
        } else if (replace_once_pressed || replace_pressed) {
            if (last_found_selected) {
                if (replace_buff[0])
                    editor.Insert(replace_buff);
                else
                    editor.Delete(false);
                refocus_search_window = true;
            }
            goto find_next;
        } else if (replace_all_pressed) {
            refocus_search_window = true;
            if (saved_selection) {
                //We replace in a separate buffer, to keep the search selection intact
                editor.SetSelection(saved_selection->first, saved_selection->second);
                std::string repl = editor.GetSelectedText();
                const std::string_view what = search_buff;
                const std::string_view what_to = replace_buff;
                for (size_t i = 0; i+what.size() < repl.size(); i++) {
                    const std::string_view in_text = std::string_view(repl).substr(i, what.size());
                    if (case_sensitive)
                        for (size_t u = 0; u < what.size(); /*nope*/)
                            if (const size_t l = UTF8TrailingBytes(what[u]) + 1; in_text.substr(u, l) == what.substr(u, l))
                                u += l;
                            else if (l > 1)
                                u += l; //we compare upper and lowercase letters only in ASCII. Or else we need ICU, which is way to big for this shit.
                            else if (const char c = what[u] & ~0x20; c < 0x41 || 0x5a < c || c != (in_text[u] & ~0x20))
                                goto not_equal;
                            else
                                u += 1;
                     else if (in_text!=what)
                        not_equal: continue;
                    //OK we have a match at position 'i'. Check if it is a whole word
                    if (whole_words_only) {
                        const char before = 0<i 
                            ? in_text[i-1] // may be a fractional UTF8 char -> not classified as a letter
                            : saved_selection->first.mColumn 
                            ? editor.GetText(TextEditor::Coordinates{ saved_selection->first.mLine, saved_selection->first.mColumn-1 }, saved_selection->first).front()
                            : '\0'; //selection starts at first char in line: always a word boundary
                        const char after = i + what.size() + 1 < repl.size()
                            ? repl[i + what.size() + 1]
                            : saved_selection->second.mColumn < editor.GetLineMaxColumn(saved_selection->second.mLine)
                            ? editor.GetText(saved_selection->second, TextEditor::Coordinates{ saved_selection->second.mLine, saved_selection->second.mColumn + 1 }).front()
                            : '\0'; //selection ends at a line end: always a word boundary
                        const auto is_alpha = [](char c) {
                            if ('0' <= c && c <= '9') return true;
                            if ('a' <= c && c <= 'z') return true;
                            if ('A' <= c && c <= 'Z') return true;
                            return c == '_';
                        };
                        if (is_alpha(before) || is_alpha(after))
                            continue; //not a full word
                    }
                    //OK, we need to replace
                    repl.replace(repl.begin() + i, repl.begin() + i + what.size(), what_to);
                    i += ptrdiff_t(what_to.size()) - 1;
                }
                editor.Insert(repl.c_str());
            } else {
                editor.SetCursorPosition(TextEditor::Coordinates(0, 0));
                while (editor.Find(search_buff, case_sensitive, whole_words_only,
                                   true, saved_selection, false))
                    editor.Insert(replace_buff);
            }
        }
    } else
        search_hoovered = false; //no search window visible, clear hoover flag
    if (!search_open)
        Config.show_search = false;
}

/** Displays and handles the example browser.
 * @param [in] first_frame_at_open Set to true on the frame, when the window appears.
 * @returns true if the example browser's modal dialog is open. */
bool ExampleBrowser(bool first_frame_at_open) {
    ImVec2 size = ImGui::GetMainViewport()->Size;
    size.x *= 0.9f;
    size.y *= 0.7f;
    ImGui::SetNextWindowSize(size, ImGuiCond_Once);
    ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Once, ImVec2(0.5f, 0.5f));
    ImGui::SetNextWindowSizeConstraints(ImVec2{200,100}*Config.dpi_mul, ImGui::GetMainViewport()->WorkSize);
    static bool open = true;
    static std::string title;
    static float maxLen = 200; //Width of the Listbox
    static Example* example_selected = nullptr;
    static int frame_when_opened = 0;
    if (first_frame_at_open) {
        example_selected = nullptr;
        open = true;
        frame_when_opened = ImGui::GetFrameCount();
        title = Config.C->lang->description + " Examples###Example browser";
        Config.example.lang = Config.C->lang;
        maxLen = 100;
        for (const Example& e : Config.examples->GetExamplesFor(Config.C->lang))
            maxLen = std::max(maxLen, ImGui::CalcTextSize(e.name.c_str()).x);
        maxLen += 40; //MAGIC NUMBER: cater for padding and selection tick
    }
    if (open && Config.example.HandleKeyboardShortcuts()) {
        //OK, handles
    } else if (escape_pressed)
        open = false; //exit examples.
    if (!ImGui::BeginPopupModal(title.c_str(), &open, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse))
        return false;
    ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
    ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0, 0));
    ImGui::BeginTable("ExamplesTable", 3, ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_Resizable | ImGuiTableFlags_NoSavedSettings);
    ImGui::TableSetupColumn("Select an example", ImGuiTableColumnFlags_WidthFixed, maxLen+8);
    ImGui::TableSetupColumn("Example text", ImGuiTableColumnFlags_WidthStretch, 6);
    ImGui::TableSetupColumn("Example chart", ImGuiTableColumnFlags_WidthStretch, 6);
    ImGui::TableNextRow();
    ImGui::TableNextColumn(); //Example list

    static std::vector<Example*> filtered_examples;
    static char example_search_buff[500] = "\0";
    static int current_item = -1;

    ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.f);
    ImGui::SetNextItemWidth(maxLen);
    if (ImGui::GetFrameCount() - frame_when_opened < 2) //The first frame finds this window zero-sized and the search box not visible=>it does not receive the focus. So we need to manually direct focus here also on the second frame.
        ImGui::SetKeyboardFocusHere();
    const bool text_changed = ImGui::InputText("##Search Examples", example_search_buff, sizeof(example_search_buff), ImGuiInputTextFlags_None);
    ImGui::PopStyleVar();
    if (text_changed || first_frame_at_open)
        filtered_examples = Config.examples->Filter(example_search_buff, Config.C->lang);

    current_item = between(current_item, 0, std::max(0, (int)filtered_examples.size()-1));
    ImGui::BeginListBox("##Filtered Examples", ImVec2(maxLen, ImGui::GetContentRegionAvail().y));
    int index = 0;
    for (const Example* e : filtered_examples) {
        if (ImGui::MenuItem(e->name.c_str(), "", current_item==index))
            current_item = index;
        Help(e->explanation.empty() ? e->keywords.c_str() : (e->explanation+"\nkeywords: "+e->keywords).c_str());
        index++;
    }
    ImGui::EndListBox();
    //Recompile chart if selection has changed
    Example* new_example_selected = filtered_examples.empty() ? nullptr : filtered_examples[current_item];
    if (example_selected != new_example_selected) {
        if (example_selected) //save the modified state
            example_selected->text = Config.example.editor.GetText();
        Config.example.ResetToNew(new_example_selected ? new_example_selected->text : "");
        Config.example.compiled_at = {-1};
        example_selected = new_example_selected;
    }

    ImGui::TableNextColumn();
    ImGui::PushFont(GetFont(false, false, false));
    ImGui::SetNextItemWidth(200);
    const bool finished_compiling = Config.example.CompileTakeResult(false);
    Config.example.DoEditor("Example Editor");
    ImGui::PopFont();

    ImGui::TableNextColumn();
    ImDrawList* const draw_list = ImGui::GetWindowDrawList();
    ImVec2 canvas_sz = ImGui::GetContentRegionAvail();
    draw_list->AddRectFilled(ImGui::GetCursorScreenPos(), ImGui::GetCursorScreenPos()+canvas_sz, IM_COL32(244, 244, 244, 255));
    const auto [image_viewport_origin, full_canvas_sz] = Config.example.DoChart(true, false, finished_compiling, true);
    ImGui::EndTable();
    ImGui::PopStyleVar(2);

    Config.example.DoSearchReplace(image_viewport_origin, full_canvas_sz);

    ImGui::EndPopup();
    return true;
}

GUIWindowGeometry InitGUI(const GUIInit& _G, float dpi_mul_, const std::string& settings_dir, const std::string& current_dir,
                          const std::vector<std::string>& example_dirs, std::vector<std::string>&& docs) {
    Config.Init(_G, dpi_mul_, std::move(docs), settings_dir, current_dir);
    const auto [no_examples, example_dir] = Config.examples->Load(example_dirs, _G.file_open_proc);
#ifdef _DEBUG
    if (no_examples)
        printf("Found %u examples in %s.\n", no_examples, example_dir.c_str());
#endif
    return Config.main_window_geometry.scale_by(Config.dpi_mul);
}

/** Call this at the beginning of every frame.
 * We regenerate the fonts if the DPI has changed.
 * @param [in] dpi_mul The DPI multiplier, larger for larger DPI screens. */
bool PreFrameGUI(float dpi_mul) {
    Config.C->PreFrame();
    Config.example.PreFrame();
    if (Config.dpi_mul == dpi_mul) return false;
    auto &io = ImGui::GetIO();
    io.Fonts->Clear();
    Config.dpi_mul = dpi_mul;
    MscGenEditorFontSize = 13 * dpi_mul;
    MscGenWindowFontSize = 16 * dpi_mul;
    LoadFonts();
    for (EditorChartData &c : Config.charts)
        c.SetFonts();
    Config.example.SetFonts();
    Config.SetFileDialogColors();
    bookmarkPaneWith = 150 * dpi_mul;
    filterComboWith = 300 * dpi_mul;
    return true;
}

/** The main routine displaying the GUI and handling it every frame. */
GUIReturn DoGUI(const GUIEvents &events) {
    //The main state of the GUI: what (modal) dialog is open
    static enum class State {
        Idle,       //No big dialog is open. Search or Rename may be
        Welcome,    //The Welcome screen is on
        FileNew,    //The New File dialog is open
        FileOpen,   //The File Open dialog is open
        PasteClipboard,//We are pasting clipboard content (potentially showing error)
        AutoPasteClipboard, //We are pasting clipboard content due to the auto paste feature
        CopyToClipboard, //We are taking a snapshot of the current chart and copy it to the clipboard as an Art:GVML ClipFormat object
        CopyToClipboardAsText, //We are taking a snapshot of the current chart and copy it to the clipboard as escaped text to be placed in alt-text
        FileSave,   //The File Save dialog is open
        FileSaveAs, //The File Save dialog is open in Save As... mode
        FileExport, //We export the file silently
        FileExportAs,//The File Export dialog is open
        Help,       //The help is open
        Example     //The example browser is open
    } main_state = State::Welcome;

    //In Welcome and FileNew states we use this to check what language to use
    //We initialize it to the value of the -S switch assuming the user wants an empty
    //chart of that type.
    static const LanguageData* lang_to_use = Config.G->languages.GetLanguage(Config.G->type);

    if (Config.source == ConfigData::Source::Empty)
        main_state = State::Welcome;

    Config.exit_requested |= events.request_exit;

    //calculate CPU usage. Smooth value
    static double smoothed_cpu_usage = 0;
    static std::chrono::microseconds last_total_cpu = std::chrono::microseconds::zero();
    static auto last_cpu_calc = std::chrono::steady_clock::now();
    if (events.total_cpu) {
        const auto now = std::chrono::steady_clock::now();
        const std::chrono::microseconds elapsed_us = std::chrono::duration_cast<std::chrono::microseconds>(now-last_cpu_calc);
        if (events.total_cpu>last_total_cpu && elapsed_us>0us) {
            const double cpu = (double)(*events.total_cpu-last_total_cpu).count()/(double)elapsed_us.count();
            last_cpu_calc = now;
            last_total_cpu = *events.total_cpu;
            smoothed_cpu_usage = cpu - (cpu - smoothed_cpu_usage) * pow(0.7, elapsed_us.count()/1e6); //smooth
        }
    }
    //Save files dropped on us locally
    static GUIEvents::Drop dropped_on_us;
    if (events.dropped) {
        if (dropped_on_us) GUIBeep();
        dropped_on_us = events.dropped;
    }

    ImGui::PushFont(GetFont(true, false, false));
    if (main_state==State::Welcome && !dropped_on_us) {
        static RecentFile open_on_startup{Config.G->file_to_open_path, Config.G->file_to_open_name};
        static bool text_on_startup = !Config.G->start_chart_text.empty();
        if (open_on_startup.path.size())
            switch (Config.Load(&open_on_startup)) {
            case StatusRet::Completed: Config.C->shift_focus_to_editor = true; main_state = State::Idle;  break;
            case StatusRet::Cancelled: open_on_startup.clear(); break;
            case StatusRet::Ongoing: break;
            }
        if (text_on_startup) { //Text has already been included in the newly opened editor in ConfigData::Init()
            main_state = State::Idle;
            text_on_startup = false;
        } else if (open_on_startup.path.empty()) //re-test: cleared on failure above
            main_state = State::FileNew;
    }
    DeadTextures.Cleanup();

    //Handle clipboard
    bool focus_app_window = false;
    static bool is_first_frame = true;
    if (auto format_content = GetClipboard(); format_content) {
        bool new_content = false;
        switch (format_content->first) {
        case ClipboardFormat::Escaped_Text:
            if (EmbedChartData chart = EmbedChartData::ParseEscapedText(format_content->second); !chart.empty()) {
                Config.clipboard_seen = std::move(chart);
                new_content = true;
            }
            break;
        case ClipboardFormat::Art_GVML:
            if (PptClipboard clip(std::move(format_content->second)); clip.size()) {
                Config.clipboard_seen = std::move(clip);
                new_content = true;
            }
            break;
        case ClipboardFormat::Else:
            Config.clipboard_seen = {};
        }
        if (new_content && Config.auto_paste_clipboard
                && (main_state==State::Idle || main_state==State::Welcome || main_state==State::FileNew)
                && !dropped_on_us
                && !Config.IsDirty()
                && !is_first_frame) {
            main_state = State::AutoPasteClipboard;
            focus_app_window = true;
        }
    }

    const auto [shift, ctrl, alt] = GetShiftKeys();
    escape_pressed = !ctrl && !alt && !shift && ImGui::IsKeyPressed(ImGuiKey_Escape); //make it globally available
    Config.C->editor.SetHandleKeyboardInputs(true); //In a previous frame we may have disabled it (when user presses Ctrl+Space)
    Config.example.editor.SetHandleKeyboardInputs(true); //In a previous frame we may have disabled it (when user presses Ctrl+Space)
    if ((main_state==State::Idle || main_state==State::Welcome || main_state==State::FileNew) && dropped_on_us) { //If we are idle or in the welcome screen and something dropped on us
        if (dropped_on_us.full_names.size()>1) {
            if (GUIError("Drop only a single file on me."))
                dropped_on_us.clear();
        } else {
            RecentFile load_this; load_this.set(dropped_on_us.full_names.front());
            switch (Config.Load(&load_this)) {
            case StatusRet::Completed: Config.C->shift_focus_to_editor = true; main_state = State::Idle; FALLTHROUGH;
            case StatusRet::Cancelled: dropped_on_us.clear(); break;
            default:
            case StatusRet::Ongoing: break;
            }
        }
    } else if (main_state == State::Idle) {    //Handle keyboard shortcuts only if we do not have some dialog open
        if (Config.C->HandleKeyboardShortcuts()) {
            //OK, do nothing
        } else if (escape_pressed) {
            Config.C->shift_focus_to_editor = true;
        } else if (!ctrl && !alt && !shift
                   && (ImGui::IsKeyPressed(ImGuiKey_F2)
                       || ImGui::IsKeyPressed(ImGuiKey_F5))) {
            Config.C->StartCompile();
        } else if (ctrl && !alt && shift && ImGui::IsKeyPressed(ImGuiKey_S)) {
            main_state = State::FileSaveAs;
        } else if (ctrl && !alt && !shift && ImGui::IsKeyPressed(ImGuiKey_S)) {
            main_state = State::FileSave;
        } else if (ctrl && !shift && ImGui::IsKeyPressed(ImGuiKey_N)) {
            lang_to_use = Config.C->lang;
            main_state = State::FileNew;
        } else if (ctrl && !shift && ImGui::IsKeyPressed(ImGuiKey_W)) {
            lang_to_use = nullptr;
            main_state = State::FileNew;
        } else if (ctrl && !shift && ImGui::IsKeyPressed(ImGuiKey_O)) {
            main_state = State::FileOpen;
        } else if (ctrl && !shift && ImGui::IsKeyPressed(ImGuiKey_E)) {
            main_state = State::FileExport;
        } else if (ctrl && shift && ImGui::IsKeyPressed(ImGuiKey_E)) {
            main_state = State::FileExportAs;
        }
    } else if (dropped_on_us) {
        //something dropped on us while not idle.
        dropped_on_us.clear();
        GUIBeep();
    }

    bool fit_to_window = false; //signal that this is needed
    bool fit_to_width = false; //signal that this is needed

    auto &io = ImGui::GetIO();
    ImGui::SetNextWindowPos(ImVec2(0, 0));
    ImGui::SetNextWindowSize(io.DisplaySize);
    ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {4, 0});
    ImGui::Begin("Main Window", nullptr,
                 ImGuiWindowFlags_NoDecoration                                        //to occupy the entire drawing area
                 | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse  //to prevent 1-bit accidental scrolling
                 | ImGuiWindowFlags_NoBringToFrontOnFocus                             //to allow the toolbox window in presentation mode to be on top
                 | ImGuiWindowFlags_NoNavInputs);                                     //to avoid pressing escape in the editor to mess with focus

    ImGui::PopStyleVar();

    static std::optional<RecentFile> load_this;
    if (!Config.presentation_mode) {
        ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, {4, 4});
        ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {8, 0});
        const bool show_compile = !Settings.instant_compile;
        const bool show_pages = Config.C->pChart && Config.C->pChart->GetPageNum() > 1;
        const int columns = 5 + int(show_compile) + int(show_pages);
        if (ImGui::BeginTable("ribbon", columns, ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingFixedFit)) {
            ImGui::TableSetupColumn("File...", ImGuiTableColumnFlags_WidthFixed);
            if (show_compile) ImGui::TableSetupColumn("Compile", ImGuiTableColumnFlags_WidthFixed);
            ImGui::TableSetupColumn("Zoom", ImGuiTableColumnFlags_WidthFixed);
            ImGui::TableSetupColumn("Clipboard", ImGuiTableColumnFlags_WidthFixed);
            if (show_pages) ImGui::TableSetupColumn("Pages", ImGuiTableColumnFlags_WidthFixed);
            ImGui::TableSetupColumn("FPS", ImGuiTableColumnFlags_WidthStretch);
            ImGui::TableSetupColumn("Help", ImGuiTableColumnFlags_WidthFixed);
            ImGui::TableNextRow();

            ImGui::TableNextColumn();
            ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32_BLACK_TRANS);
            if (ImGui::Button("File...") && main_state == State::Idle)
                ImGui::OpenPopup("FileMenu");
            ImGui::SameLine();
            if (ImGui::Button("Settings...") && main_state == State::Idle)
                ImGui::OpenPopup("Settings");

            ImGui::PopStyleVar(2);
            if (ImGui::BeginPopup("FileMenu")) {
                if (ImGui::MenuItem("Welcome...", "Ctrl+W", nullptr)) main_state = State::FileNew;
                Help("Opens the Welcome screen, where you can open files or start new ones.");
                if (ImGui::MenuItem("Open...", "Ctrl+O", nullptr)) main_state = State::FileOpen;
                Help("Opens a File picker, where you can select a file to open.");
                if (ImGui::MenuItem("Save...", "Ctrl+S", nullptr)) main_state = State::FileSave;
                if (Config.source == ConfigData::Source::Clipboard)
                    Help("Copies the current chart back to the clipboard.");
                else if (Config.file.path.empty())
                    Help("Saves he chart to disk as a new file.");
                else
                    Help(("Saves the file to "+Config.file.path).c_str());
                if (ImGui::MenuItem("Save As...", "Shift+Ctrl+S", nullptr)) main_state = State::FileSaveAs;
                Help("Saves the chart to the disk under another name.");
                ImGui::BeginDisabled(Config.last_export_file.path.empty());
                if (ImGui::MenuItem(("Export again as "+Config.last_export_file.name).c_str(), "Ctrl+E", nullptr)) main_state = State::FileExport;
                Help(("Exports with previous settings to "+Config.last_export_file.path).c_str());
                ImGui::EndDisabled();
                if (ImGui::MenuItem("Export As...", "Shift+Ctrl+E", nullptr)) main_state = State::FileExportAs;
                Help("Saves the chart in a graphical format, PNG, SVG, EPS, PDF or PPT.");
                if (ImGui::MenuItem("Exit", "", nullptr)) Config.exit_requested = true;
                Help("Leave Msc-generator. If the chart being edited has outstanding changes, you will have the option to save. "
                     "If you opt to not save outstanding changes, they will be saved and you can recover working the next "
                     "time you start Msc-generator.");
                ImGui::Separator();
                if (ImGui::MenuItem("Copy to Clipboard", "", nullptr)) main_state = State::CopyToClipboard;
                Help("Copies the whole chart (including its graphical representation) as an OOXML object "
                     "that can be inserted into Office applications and re-opened by Msc-generator later. "
                     "To open such an chart, simply copy it to the clipboard and paste it to Msc-generator.");
                if (ImGui::MenuItem("Copy to Clipboard as alt-text", "", nullptr)) main_state = State::CopyToClipboardAsText;
                Help("Copies the text-encoded chart text to the clipboard. Adding this to any Office image "
                     "as 'Alt Text' will enable opening that graphic as an embedded chart.");
                ImGui::BeginDisabled(Config.clipboard_seen.index()==0);
                if (ImGui::MenuItem("Paste Clipboard", "", nullptr)) main_state = State::PasteClipboard;
                ImGui::EndDisabled();
                Help("If the clipboard contains a chart copied from Office apps or a suitable encoded alt-text string "
                     "then paste this content into Msc-generator.");
                ImGui::Separator();
                for (auto& lname : Config.G->languages.GetLanguages())
                    if (auto* l = Config.G->languages.GetLanguage(lname)
                        ; (ImGui::MenuItem(("New "+l->description+"...").c_str(),
                                           l==Config.C->lang ? "Ctrl+N" : "", nullptr))) {
                        lang_to_use = l;
                        main_state = State::FileNew;
                    }
                ImGui::Separator();
                load_this = Config.RecentFilesAsMenu();
                ImGui::EndPopup(); //FileMenu
            }
            ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, {4, 4});
            ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {8, 0});

            if (ImGui::BeginPopup("Settings", ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoScrollbar
                                  | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_AlwaysAutoResize)) {
                ImGui::PopStyleColor(); //show button background
                ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {8, 4});
                ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1);
                if (ImGui::BeginTable("Compiling", 3, ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_SizingFixedFit
                                      | ImGuiTableFlags_BordersInnerV)) {
                    ImGui::TableSetupColumn("Compile", ImGuiTableColumnFlags_WidthFixed, 180*Config.dpi_mul);
                    ImGui::TableSetupColumn("View", ImGuiTableColumnFlags_WidthFixed, 180*Config.dpi_mul);
                    ImGui::TableSetupColumn("Editor", ImGuiTableColumnFlags_WidthFixed, 180*Config.dpi_mul);
                    ImGui::TableNextRow();
                    ImGui::PushFont(GetFont(true, true, false));
                    ImGui::TableSetColumnIndex(0);
                    ImGui::Text("Compilation");
                    ImGui::TableSetColumnIndex(1);
                    ImGui::Text("View");
                    ImGui::TableSetColumnIndex(2);
                    ImGui::Text("Editor");
                    ImGui::PopFont();

                    ImGui::TableNextRow(); //of the Settings, row containing the checkboxes
                    ImGui::TableSetColumnIndex(0);
                    ImGui::Checkbox("Instant compilation", &Settings.instant_compile);
                    Help("When set, the chart will automatically be recompiled every time a change is made. "
                         "When unset, the 'Compile' button or F2 or F5 needs to be pressed to update the chart view. "
                         "The latter is useful for complex charts that take longer to compile.");
                    if (ImGui::Checkbox("Pedantic", &Settings.pedantic)) {
                        Config.C->editor.ReColor();
                        Config.C->StartCompile();
                    }
                    Help("When pedantic is set Msc-generator enforces stricter language interpretation. "
                         "For signalling charts, it means generating a warning if an entity is not declared explicitly before use. "
                         "For graphs, it means generating a warning for each use of directed edges in undirected graphs and vice versa "
                         "(graphviz does not allow such mixing), but when turned off, mixing directed and undirected edges in a graph becomes possible. "
                         "For Block Diagrams, it prevents auto-generating an block when mentioned without "
                         "a 'box' or similar keyword. This allows forward referencing blocks in arrow "
                         "definitions(by avoiding their creation instead of referencing a later definition.)");
                    if (ImGui::Checkbox("Technical info", &Settings.technical_info))
                        Config.C->StartCompile();
                    Help("When set a few compilation statistics are added to the Error/Warning list.");

                    static int current_forced_design = []()->int {
                        //If the user specified a design on the command line, make that the start index
                        if (Config.C && Config.C->lang)
                            if (auto i = std::ranges::find_if(Config.C->lang->designs, [](auto& pair) {return pair.first == Settings.forced_design; }); i != Config.C->lang->designs.end())
                                return std::distance(Config.C->lang->designs.begin(), i)+1;
                        return 0;
                    }();
                    std::string items_separated_by_zero = "(chart defined)\0"s;
                    const bool has_designs = Config.C->lang && Config.C->lang->designs.size();
                    if (has_designs)
                        for (const auto &[name, _] : Config.C->lang->designs)
                            items_separated_by_zero.append(name).push_back('\0');
                    ImGui::Text("Design:"); ImGui::SameLine();
                    ImGui::BeginDisabled(!has_designs);
                    if (ImGui::Combo("##Design", &current_forced_design, items_separated_by_zero.c_str(), 5)) {
                        if (current_forced_design)
                            Settings.forced_design
                                = Config.G->languages.cshParams->ForcedDesign
                                = std::next(Config.C->lang->designs.begin(), current_forced_design-1)->first;
                        else
                            Settings.forced_design = Config.G->languages.cshParams->ForcedDesign = {};
                        Config.C->StartCompile();
                    }
                    ImGui::EndDisabled();
                    Help("The design selected here will be applied to the chart (overriding its selection).");

                    static int current_forced_layout = 0;
                    static const auto layout_algos = [] { auto set = GetLayoutMethods(); return std::vector<std::string>(set.begin(), set.end()); }();
                    items_separated_by_zero = "(graph defined)\0"s;
                    const bool has_layouts = Config.C->lang && Config.C->lang->GetName()=="graph";
                    if (has_layouts)
                        for (const std::string &name: layout_algos)
                            items_separated_by_zero.append(name).push_back('\0');
                    ImGui::Text("Layout:"); ImGui::SameLine();
                    ImGui::BeginDisabled(!has_layouts);
                    if (ImGui::Combo("##Layout", &current_forced_layout, items_separated_by_zero.c_str(), 5)) {
                        if (current_forced_layout)
                            Settings.forced_layout = layout_algos[current_forced_layout-1];
                        else
                            Settings.forced_layout.clear();
                        Config.C->StartCompile();
                    }
                    ImGui::EndDisabled();
                    Help("The layout algorithm selected here will be applied to the graph (overriding its selection).");

                    ImGui::TableSetColumnIndex(1); //of the Settings, row containing the checkboxes
                    if (ImGui::Checkbox("Fit to Window", &Settings.do_fit_window))
                        if (Settings.do_fit_window) {
                            Settings.do_fit_width = false;
                            fit_to_window = true;
                        }
                    Help("Make the entire chart visible after each compilation.");
                    if (ImGui::Checkbox("Fit to Width", &Settings.do_fit_width))
                        if (Settings.do_fit_width) {
                            Settings.do_fit_window = false;
                            fit_to_width = true;
                        }
                    Help("Fit the chart to the width of the window after each compilation.");
                    static float slider_zoom = 100;
                    ImGui::SetNextItemWidth(180*Config.dpi_mul);
                    if (ImGui::SliderFloat("##Zoom", &slider_zoom, EditorChartData::min_zoom*100, EditorChartData::max_zoom*100,
                                           "Zoom: %.0f%%", ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic)) {
                        Config.C->SetZoom(slider_zoom/100);
                        Config.C->fitted_to_width = Config.C->fitted_to_window = false; //To prevent re-fitting when Settings.do_fit_window/width is set.
                    } else
                        slider_zoom = Config.C->zoom * 100;
                    Help("You can also use Ctrl+mouse wheel while hoovering over the chart area to zoom the chart.");
                    ImGui::Checkbox("Tracking mode", &Config.C->tracking_mode);
                    Help("In tracking mode each chart element hoovered above will be highlighted "
                         "and its corresponding text will be selected in the text editor. Likewise "
                         "if you move around in the editor, the chart element the cursor is currently "
                         "in will be highlighted.\n"
                         "In addition, you can mark chart elements by clicking on them. This allows "
                         "easy explanation over a screen sharing session.\n"
                         "Tracking mode can also be turned on by double-clicking the chart area and "
                         "can be turned off by pressing Esc.");
                    ImGui::BeginDisabled(Config.C->lang && !Config.C->lang->has_autoheading);
                    ImGui::Checkbox("Auto Heading", &Config.auto_heading);
                    ImGui::EndDisabled();
                    Help("For signalling charts only: always show the entities at the top irrespective "
                         "of scroll and zoom status.");
                    ImGui::BeginDisabled(Config.C->lang && !Config.C->lang->has_element_controls);
                    ImGui::Checkbox("Collapse/Expand Controls", &Settings.show_controls);
                    Help("Show collapse/expand controls for boxes/groups.");
                    ImGui::EndDisabled();
                    ImGui::PopStyleVar(); //end of framesize=1
                    if (ImGui::Button("Presentation mode"))
                        Config.presentation_mode = true;
                    Help("Show the chart only in full screen. Good for presenting about a chart. "
                         "Press Esc to exit full screen mode.");

                    ImGui::TableSetColumnIndex(2); //of the Settings, row containing the checkboxes
                    ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1);
                    ImGui::Checkbox("Auto Save", &Config.auto_save);
                    Help("Automatically save the document every second. "
                         "Charts embedded in PNG/PPT files are saved with the picture of last compilation, "
                         "but are not recompiled on save. (You can set 'Instant Compilation' to "
                         "recompile after each change.");
                    ImGui::Checkbox("Auto Paste Clipboard", &Config.auto_paste_clipboard);
                    Help("Whenever an Msc-generator chart is copied to the clipboard and the "
                         "current chart is not dirty, we automatically paste the content of the clipboard "
                         "and raise and focus the Msc-generator window. This is in effect automates the "
                         "Pressing of the 'Paste Clipboard' button.");
                    ImGui::Text("Color Scheme:"); ImGui::SameLine();
                    ImGui::SetNextItemWidth(90*Config.dpi_mul);
                    if (ImGui::Combo("##Color Scheme", &Config.G->languages.cshParams->color_scheme,
                                     "Minimal\0Standard\0Colorful\0Error-oriented\0", 4)) {
                        Config.C->editor.SetPalette(MscCshAppearanceList[Config.G->languages.cshParams->color_scheme]);
                        Config.example.editor.SetPalette(MscCshAppearanceList[Config.G->languages.cshParams->color_scheme]);
                    }
                    ImGui::Checkbox("Automatic Hints", &Config.auto_hint);
                    Help("Automatically brings up hints as you type. If turned off hints are only "
                         "given if you press Ctrl+Space.");
                    //ImGui::Text("Hint filtering:"); ImGui::SameLine();
                    //ImGui::SetNextItemWidth(90*Config.dpi_mul);
                    //if (ImGui::Combo("##Hint Filtering", &Config.main.filter_hints, "None\0Fuzzy\0Substring\0Start with\0", 4))
                    //    Config.example.filter_hints = Config.main.filter_hints;
                    //Help("None: no filtering or sorting is done: all hints shown in alphabetical order\n"
                    //     "Fuzzy: fuzzy matching, sorted by relevance, with irrelevant hints removed\n"
                    //     "Substring: Filter and sort hints based on how long part of the word typed matches them\n"
                    //     "Start with: Sort and filter hints by how long the typed word matches their beginning");
                    if (ImGui::Checkbox("Error squiggles", &Config.error_squiggles))
                        Config.C->editor.ReColor();
                    Help("Underlines problematic areas of chart text and provides explanation in a tooltip. "
                         "This works even if the chart is not compiled, so this is useful for larger charts "
                         "with instant compilation turned off. Note that compilation results in more "
                         "comprehensive error diagnostics.");
                    if (ImGui::Checkbox("Smart Indent", &Config.smart_indent))
                        Config.C->editor.mIndent = Config.example.editor.mIndent
                            = Config.smart_indent ? &EditorChartData::SmartIndentStatic : nullptr;
                    Help("Automatically adjust indentation when pressing Tab, Enter, {, [ or typing a label.");
                    static float editor_slider_zoom = 100;
                    ImGui::SetNextItemWidth(180*Config.dpi_mul);
                    if (ImGui::SliderFloat("##FontScale", &editor_slider_zoom,
                                           EditorChartData::min_editor_scale*100, EditorChartData::max_editor_scale*100,
                                           "Font scale: %.0f%%", ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic)) {
                        Config.C->SetFontScale(editor_slider_zoom/100);
                        Config.example.SetFontScale(editor_slider_zoom/100);
                    } else
                        editor_slider_zoom = Settings.editor_font_scale * 100;
                    Help("You can also use Ctrl+mouse wheel while hoovering over the text editor to scale the editor font.");

                    ImGui::PopStyleVar(); //end of framesize=1
                    ImGui::EndTable(); //Settings
                }
                ImGui::PopStyleVar(); //item spacing
                ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32_BLACK_TRANS);
                ImGui::EndPopup(); //Settings
            }

            if (show_compile) {
                ImGui::TableNextColumn(); //of the ribbon
                ImGui::BeginDisabled(Settings.instant_compile || Config.C->IsCompiled());
                if (ImGui::Button("Compile"))
                    Config.C->StartCompile();
                ImGui::EndDisabled();
            }
            ImGui::TableNextColumn(); //of the ribbon
            ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {0, 0});
            ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1);
            if (ImGui::Checkbox("##Fit to window", &Settings.do_fit_window))
                if (Settings.do_fit_window) {
                    Settings.do_fit_width = false;
                    fit_to_window = true;
                }
            Help("Make the entire chart visible after each compilation.");
            ImGui::PopStyleVar();
            ImGui::SameLine();
            ImGui::BeginDisabled(Config.C->fitted_to_window);
            if (ImGui::Button("Fit to Window")) {
                fit_to_window = true;
                Config.C->fitted_to_width = false; //to prevent automatically re-fit to width if Config.do_fit_width
            }
            Help("Make the entire chart visible now.");
            ImGui::EndDisabled();
            ImGui::PopStyleVar();
            ImGui::SameLine();
            ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1);
            if (ImGui::Checkbox("##Fit to width", &Settings.do_fit_width))
                if (Settings.do_fit_width) {
                    Settings.do_fit_window = false;
                    fit_to_width = true;
                }
            Help("Fit the chart to the width of the window after each compilation.");
            ImGui::PopStyleVar();
            ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {0, 0});
            ImGui::SameLine();
            ImGui::BeginDisabled(Config.C->fitted_to_width);
            if (ImGui::Button("Fit to Width")) {
                fit_to_width = true;
                Config.C->fitted_to_window = false; //to prevent automatically re-fit to window if Settings.do_fit_window
            }
            Help("Fit the chart to the width of the window now.");
            ImGui::EndDisabled();

            ImGui::TableNextColumn(); //of the ribbon
            ImGui::BeginDisabled(Config.clipboard_seen.index()==0);
            if (ImGui::Button("Paste Clipboard") && main_state==State::Idle)
                main_state = State::PasteClipboard;
            PasteClipBoardHelp();
            ImGui::EndDisabled();
            ImGui::PopStyleVar();
            ImGui::PopStyleColor();
            if (show_pages) {
                ImGui::TableNextColumn();  //of the ribbon
                PageSelector();
            }

            ImGui::TableNextColumn(); //of the ribbon
            if (events.total_cpu)
                ImGui::TextDisabled("%02.0f FPS, %03.0f%% CPU, coloring: %dms, compile: %dms", io.Framerate, smoothed_cpu_usage*100,
                                    (int)Settings.last_csh_time.count(), (int)Settings.last_compile_time.count());
            else
                ImGui::TextDisabled("%02.0f FPS, coloring: %dms, compile: %dms", io.Framerate,
                                    (int)Settings.last_csh_time.count(), (int)Settings.last_compile_time.count());

            ImGui::TableNextColumn(); //of the ribbon
            ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32_BLACK_TRANS);
            if (Config.examples->HasExamplesFor(Config.C->lang)) { //Show examples only if we have examples for this language loaded
                bool example_pressed = false;
                if (ImGui::Button("Examples")) {
                    ImGui::OpenPopup("###Example browser");
                    example_pressed = true;
                    main_state = State::Example;
                }
                if (!ExampleBrowser(example_pressed) && main_state == State::Example) {
                    main_state = State::Idle;
                    Config.C->shift_focus_to_editor = true;
                }
                ImGui::SameLine();
            }
            static bool help_is_open = false, licence_is_open = false;
            if (ImGui::Button("Help")) {
                ImGui::OpenPopup("Help");
                main_state = State::Help;
                help_is_open = true;
            }
            ImGui::PopStyleColor();
            help_is_open &= !escape_pressed;
            if (ImGui::BeginPopupModal("Help", &help_is_open, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar |
                                       ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings)) {
                ImGui::PushTextWrapPos(ImGui::GetFontSize()*35);
                ImGui::Text("These are the things you should try to discover all the features of this GUI.");
                ImGui::BulletText("Pressing F2 or F5 for compiling the chart. (If you turn instant compilation on, then this is not necessary.)");
                ImGui::BulletText("Dragging the chart to pan it. Ctrl+mouse wheel while hoovering in the chart to zoom it.");
                ImGui::BulletText("Clicking an element in the chart to jump to it in the chart text.");
                ImGui::BulletText("Pressing F4 in the chart text to highlight the element under the cursor on the chart.");
                ImGui::BulletText("Double clicking the chart to enter tracking mode.");
                ImGui::BulletText("Pressing Ctrl+Space to get hints on chart syntax and language. (Also try automatic hints.)");
                ImGui::BulletText("Pressing Tab to auto indent a line (or lines in a selection).");
                ImGui::BulletText("Pressing Ctrl+F for search and replace.");
                ImGui::BulletText("Right click the editor for a context menu.");
                ImGui::BulletText("See the examples (with keyword search) to get tips on language syntax and features.");
                ImGui::BulletText("Use 'File|Copy to Clipboard' to embed a chart into Office documents (paste them in).\n"
                                  "Then Copy the inserted chart to the clipboard in PowerPoint or Word to edit in Msc-generator.");
                ImGui::TextUnformatted("");

                static std::string doc_err;
                static bool doc_err_is_open = false;
                if (Config.Docs.size()) {
                    ImGui::AlignTextToFramePadding();
                    ImGui::TextUnformatted("Detailed documentation:"); ImGui::SameLine();
                    for (const std::string& doc_name : Config.Docs) {
                        if (ImGui::Button((doc_name+" help").c_str()))
                            if (auto err = ShowDocumentation(doc_name)) {
                                doc_err = std::move(*err);
                                ImGui::OpenPopup("Cannot open documentation");
                                doc_err_is_open = true;
                            }
                        if (&doc_name!=&Config.Docs.back()) ImGui::SameLine();
                    }
                }
                ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f));
                ImGui::SetNextWindowSize({std::min(ImGui::CalcTextSize(doc_err.c_str()).x + 10, ImGui::GetMainViewport()->WorkSize.x * .5f), 0});
                if (ImGui::BeginPopupModal("Cannot open documentation", &doc_err_is_open, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar |
                                           ImGuiWindowFlags_NoCollapse| ImGuiWindowFlags_NoSavedSettings)) {
                    ImGui::TextWrapped("%s", doc_err.c_str());
                    ImGui::EndPopup();
                }

                ImGui::TextUnformatted("");
                std::string v = version(Config.G->languages);
                std::ranges::replace(v, '\n', '\0');
                for (size_t pos = 0; pos<v.length(); /*nope*/) {
                    ImGui::TextUnformatted(v.c_str()+pos);
                    pos = v.find('\0', pos);
                    if (pos == v.npos) break;
                    else pos++;
                }

                ImGui::SameLine(ImGui::GetWindowContentRegionWidth()-ImGui::CalcTextSize("Show license").x);
                if (ImGui::Button("Show license")) {
                    ImGui::OpenPopup("License");
                    licence_is_open = true;
                }
                const std::string lic = license(Config.G->languages);
                ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f));
                ImGui::SetNextWindowSizeConstraints({ImGui::CalcTextSize(lic.c_str()).x, 0}, ImGui::GetMainViewport()->WorkSize*0.8f);
                if (ImGui::BeginPopupModal("License", &licence_is_open, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar |
                                           ImGuiWindowFlags_NoCollapse| ImGuiWindowFlags_NoSavedSettings)) {
                    ImGui::TextUnformatted(lic.c_str());
                    ImGui::EndPopup();
                }
                ImGui::PopTextWrapPos();
                ImGui::EndPopup();
            } else if (main_state == State::Help) {
                main_state = State::Idle;
                Config.C->shift_focus_to_editor = true;
            }
            ImGui::EndTable();
        }
        ImGui::PopStyleVar(2);
    } //!presentation_mode

    switch (main_state) {
    case State::FileNew:
        //If the user did not specify a language, we show the welcome screen.
        if (Config.New(lang_to_use==nullptr, lang_to_use)!=StatusRet::Ongoing) {
            Config.C->shift_focus_to_editor = true;
            main_state = State::Idle;
            lang_to_use = nullptr;
        }
        break;

    case State::FileOpen:
        if (Config.Load()!=StatusRet::Ongoing) main_state = State::Idle;
        break;

    case State::PasteClipboard:
    case State::AutoPasteClipboard:
        switch (Config.Load(&Config.clipboard_seen)) {
        case StatusRet::Completed:
            Config.AddMessage({ (main_state==State::AutoPasteClipboard ? "Auto-p" : "P") + std::string("asted from clipboard.")
                                + (Config.source==ConfigData::Source::Clipboard ? " Use 'Save' to copy the content\nback to the clipboard (keeping the original context of the chart)." : ""),
                              Config.source==ConfigData::Source::Clipboard ? 8.f : 5.f });
            FALLTHROUGH;
        case StatusRet::Cancelled:
            main_state = State::Idle;
            FALLTHROUGH;
        case StatusRet::Ongoing:
            break;
        }
        break;

    case State::FileSave:
    case State::FileSaveAs:
    case State::CopyToClipboard:
        if (const ConfigData::SaveMode mode =
                      main_state==State::CopyToClipboard ? ConfigData::SaveMode::CopyToClipboard
                    : main_state==State::FileSaveAs ? ConfigData::SaveMode::As
                    : ConfigData::SaveMode::Interactive
                 ; Config.Save(mode)!=StatusRet::Ongoing)
            main_state = State::Idle;
        break;

    case State::CopyToClipboardAsText:
        if (EmbedChartData data{ .chart_type = Config.C->lang->GetName(), .chart_text = Config.C->editor.GetText(),
                                 .chart_size = Config.C->pChart ? Config.C->pChart->GetCanvasSize() : XY(0,0),
                                 .gui_state = Config.C->pChart->SerializeGUIState(),
                                 .page = Config.C->page }
                ; CopyToClipboard(ClipboardFormat::Escaped_Text, data.Escape())) {
            Config.AddMessage({ "Copied alt-text to clipboard", 5 });
            main_state = State::Idle;
        }
        break;

    case State::FileExport:
    case State::FileExportAs:
        if (Config.Export(main_state==State::FileExport)!=StatusRet::Ongoing) main_state = State::Idle;
        break;

    case State::Idle:
        if (load_this && Config.Load(&*load_this)!=StatusRet::Ongoing) {//something selected from a Recent Files list
            load_this.reset();
            break;
        }
        FALLTHROUGH;
    case State::Example:
    case State::Help:
        if (Config.auto_save
                && Config.IsDirty()
                && Config.autosave_interval < ConfigData::clock::now() - Config.last_autosave) {
           const StatusRet ret = Config.Save(ConfigData::SaveMode::Quiet);
           if (ret!=StatusRet::Ongoing) //We may still compile for PPT embedding
               Config.last_autosave = ConfigData::clock::now(); //Update timestamp even on failure - to limit the rate of retries.
        }
        break;
    default:
    case State::Welcome:
        break;
    }

    //Take the result of all charts compiling
    for (EditorChartData& c : Config.charts)
        if (const bool finished_compiling = c.CompileTakeResult(false); &c==Config.C) {
            //but react only when the current one finishes.
            if (finished_compiling) {
                fit_to_window |= Settings.do_fit_window;
                fit_to_width |= Settings.do_fit_width;
            } else {
                //if previously we were fitted, keep being so
                fit_to_window |= Config.C->fitted_to_window && Settings.do_fit_window;
                fit_to_width |= Config.C->fitted_to_width && Settings.do_fit_width;
            }
        }
    //Delete destroyed charts
    Config.PruneCharts();
    Config.UpdateWindowTitle();

    ImVec2 image_viewport_origin, full_canvas_sz;
    if (!Config.presentation_mode) {
        ImGuiTableFlags flags = ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable
            | ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_BordersOuterH | ImGuiTableFlags_SizingFixedSame;
        const bool can_be_muliple = Config.source == ConfigData::Source::Clipboard || Config.source == ConfigData::Source::PPT;
        ImGui::BeginTable("chart", 3, flags, ImGui::GetContentRegionAvail());
        ImGui::TableSetupColumn("PPT charts", ImGuiTableColumnFlags_WidthStretch, 0.2f);
        ImGui::TableSetupColumn(Config.file.name.c_str(), ImGuiTableColumnFlags_WidthStretch);
        ImGui::TableSetupColumn("Chart", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel);
        ImGui::TableSetColumnEnabled(0, can_be_muliple);
        ImGui::TableNextRow();
        ImGui::TableSetColumnIndex(0);
        if (can_be_muliple) {
            unsigned slide = 0, idx = 0;
            for (auto& c : Config.charts) {
                if (c.slide!=slide) {
                    slide = c.slide;
                    ImGui::PushFont(GetFont(true, true, false));
                    ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, ImGui::GetStyle().ItemSpacing.y));
                    if (Config.source==ConfigData::Source::Clipboard) ImGui::TextUnformatted("Clipboard");
                    else if (slide) ImGui::Text("Slide %d", slide);
                    else ImGui::TextUnformatted("<new slide>");
                    ImGui::PopFont();
                    if (slide<=Config.slides.size() && Config.slides[slide-1].length()) {
                        ImGui::SameLine();
                        ImGui::TextUnformatted(": ");
                        ImGui::SameLine();
                        ImGui::PushFont(GetFont(true, false, true));
                        ImGui::TextUnformatted(Config.slides[slide-1].c_str());
                        ImGui::PopFont();
                    }
                    ImGui::PopStyleVar();
                }
                std::string label = c.IsDirty() ? "    *" : "    ";
                label.append(c.chart ? std::to_string(c.chart) : "-");
                label.append(": ").append(c.lang->description).append("##chart").append(std::to_string(idx++)); //idx disambiguates labels
                if (ImGui::Selectable(label.c_str(), Config.C==&c))
                    Config.C = &c;
            }
            if (Config.clipboard.empty()) {
                _ASSERT(Config.source==ConfigData::Source::PPT);
                ImGui::Spacing();
                static bool adding_new_chart = false;
                if (adding_new_chart |= ImGui::Button("Add new chart")) {
                    const auto [res, slide, lang_str] = GUIAddChart("Add a new chart", Config.slides);
                    switch (res) {
                    case StatusRet::Cancelled:
                        adding_new_chart = false;
                        FALLTHROUGH;
                    default:
                    case StatusRet::Ongoing:
                        break;
                    case StatusRet::Completed:
                        Config.C = &Config.charts.emplace_back();
                        Config.C->Init(*Config.G->languages.cshParams);
                        Config.C->ResetText(Config.G->languages.GetLanguageByExtension(lang_str));
                        Config.C->saved_at = { -1, 0 }; //start as dirty
                        Config.C->slide = slide;
                        Config.C->chart = 0;
                        Config.C->page = 0;
                        Config.charts.sort([](const EditorChartData& c1, const EditorChartData& c2) { return std::pair(c1.slide-1, c1.chart-1) < std::pair(c2.slide-1, c2.chart-1); }); //unsigned -1 wraps 0 to UINT_MAX and places new charts at the end.
                        adding_new_chart = false;
                    }
                }
            }
        }
        ImGui::TableSetColumnIndex(1);
        Config.C->DoEditor("Text Editor");

        ImGui::TableSetColumnIndex(2);
        std::tie(image_viewport_origin, full_canvas_sz) =
            Config.C->DoChart(true, fit_to_window, fit_to_width, main_state == State::Idle);
        ImGui::EndTable();
        Config.C->DoSearchReplace(image_viewport_origin, full_canvas_sz);
    } else { //presentation mode
        std::tie(image_viewport_origin, full_canvas_sz) =
            Config.C->DoChart(false, fit_to_window, fit_to_width, main_state == State::Idle);
        if (Config.C->pChart && Config.C->pChart->GetPageNum()>1) {
            const ImVec2 LR = ImGui::GetMainViewport()->WorkPos + ImGui::GetMainViewport()->WorkSize;
            static ImVec2 page_selector_size(0, 0);
            ImGui::SetNextWindowPos(LR - page_selector_size - ImVec2(20, 20));
            ImGui::SetNextWindowBgAlpha(0.5f);
            ImGui::Begin("Presentation mode", &Config.presentation_mode,
                         ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoScrollWithMouse
                         | ImGuiWindowFlags_AlwaysAutoResize);
            PageSelector();
            page_selector_size = ImGui::GetWindowSize();
            ImGui::End();
        }
    }
    Config.C->fitted_to_window = Config.C->IsFittedToWindow(full_canvas_sz);
    Config.C->fitted_to_width = Config.C->IsFittedToWidth(full_canvas_sz);

    //ImGui::ShowDemoWindow();

    ImGui::End(); //Main Window
    ImGui::PopFont();

    Config.C->PruneAnimations();
    Config.example.PruneAnimations();
    Config.SaveSettings(false);
    last_frame = my_clock::now();
    is_first_frame = false;

    return Config.Ret(focus_app_window);
}

/** Call this at the end of the program. */
void ShutdownGUI(const GUIWindowGeometry& w) {
    Config.main_window_geometry = w.scale_by(1/Config.dpi_mul);
    Config.SaveSettings(true);
}
