reckfellENGINE

An engine application: documents, windows and closing

A complete C# notes editor with saving, unsaved-change protection and desktop-integration checks.

On this page

Run a notes application

Add UiSampleStyle.cs and NotesAppDemo.cs to a C# project. Attach NotesAppDemo to a separate level's Control root, build and run with F6.

Enter text, save, then change it. Closing the main window offers save, cancellation or explicit discard. Loading is refused while text is unsaved. The file is user://ui-notes.txt; ProjectSettings.GlobalizePath resolves its directory. Approved closing exits the process by default. QuitOnApproved = false exists for handler checks without stopping a test project.

NotesAppDemo.cs
C#
using System;
using Reckfell;

public partial class NotesAppDemo : Control
{
    [Export] public string FilePath { get; set; } = "user://ui-notes.txt";
    [Export] public bool QuitOnApproved { get; set; } = true;
    public TextEdit Editor { get; private set; } = null!;
    public ConfirmationDialog ClosePrompt { get; private set; } = null!;
    public AcceptDialog About { get; private set; } = null!;
    public Label Status { get; private set; } = null!;
    public bool Dirty { get; private set; }
    public int ApprovedCount { get; private set; }
    private string _saved = "";
    private Window _window = null!;
    private bool _autoAccept;

    public override void _Ready()
    {
        UiSampleStyle.Prepare(this);
        _window = GetWindow(); _autoAccept = GetTree().AutoAcceptQuit; GetTree().AutoAcceptQuit = false;
        _window.CloseRequested += RequestClose;
        var margin = new MarginContainer(); AddChild(margin); margin.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect, margin: 16);
        var column = new VBoxContainer(); margin.AddChild(column);
        var buttons = new HBoxContainer(); column.AddChild(buttons);
        AddButton(buttons, UiSampleStyle.Text("Сохранить", "Save"), () => Status.Text = SaveDocument().ToString());
        AddButton(buttons, UiSampleStyle.Text("Загрузить", "Load"), () => Status.Text = LoadDocument().ToString());
        AddButton(buttons, UiSampleStyle.Text("О программе", "About"), () => About.PopupCentered(new Vector2I(320, 150)));
        Editor = new TextEdit { SizeFlagsVertical = SizeFlags.ExpandFill }; column.AddChild(Editor);
        Status = new Label { Text = FilePath, AutowrapMode = TextServer.AutowrapMode.WordSmart }; column.AddChild(Status);
        Editor.TextChanged += UpdateDirty;
        ClosePrompt = new ConfirmationDialog
        {
            Title = UiSampleStyle.Text("Несохранённый текст", "Unsaved text"),
            DialogText = UiSampleStyle.Text("Сохранить перед выходом?", "Save before closing?"),
            DialogHideOnOk = false, Theme = Theme
        };
        AddChild(ClosePrompt); ClosePrompt.GetOkButton().Text = UiSampleStyle.Text("Сохранить и выйти", "Save and close");
        ClosePrompt.AddButton(UiSampleStyle.Text("Выйти без сохранения", "Discard and close"), action: "discard");
        ClosePrompt.Confirmed += () =>
        {
            Error result = SaveDocument(); Status.Text = result.ToString();
            if (result == Error.Ok) { ClosePrompt.Hide(); ApproveClose(); }
        };
        ClosePrompt.Canceled += () => ClosePrompt.Hide();
        ClosePrompt.CustomAction += action => { if (action == "discard") { ClosePrompt.Hide(); ApproveClose(); } };
        About = new AcceptDialog { Title = "Reckfell UI", DialogText = UiSampleStyle.Text("Пример текстового приложения", "A text application example"), Theme = Theme };
        AddChild(About);
    }
    private static void AddButton(HBoxContainer row, string text, Action action)
    {
        var button = new Button { Text = text }; row.AddChild(button); button.Pressed += action;
    }
    private void UpdateDirty() => Dirty = Editor.Text != _saved;
    public void ReplaceText(string text) { Editor.Text = text; UpdateDirty(); }
    private bool ValidPath() => FilePath.StartsWith("user://") && !FilePath.Contains("..") && FilePath.EndsWith(".txt");
    public Error SaveDocument()
    {
        if (!ValidPath() || Editor.Text.Length > 1000000) return Error.InvalidParameter;
        string target = ProjectSettings.GlobalizePath(FilePath);
        string temporary = target + ".tmp";
        try
        {
            System.IO.File.WriteAllText(temporary, Editor.Text, new System.Text.UTF8Encoding(false));
            System.IO.File.Move(temporary, target, overwrite: true);
        }
        catch (System.IO.IOException) { return Error.FileCantWrite; }
        catch (UnauthorizedAccessException) { return Error.FileCantWrite; }
        catch (ArgumentException) { return Error.InvalidParameter; }
        _saved = Editor.Text; UpdateDirty(); return Error.Ok;
    }
    public Error LoadDocument()
    {
        if (!ValidPath()) return Error.InvalidParameter;
        if (Dirty) return Error.Busy;
        if (!FileAccess.FileExists(FilePath)) return Error.FileNotFound;
        using var file = FileAccess.Open(FilePath, FileAccess.ModeFlags.Read);
        if (file is null) return FileAccess.GetOpenError();
        if (file.GetLength() > 4000000) return Error.InvalidData;
        string text = file.GetAsText(); Error error = file.GetError();
        if (error != Error.Ok && error != Error.FileEof) return error;
        _saved = text; Editor.Text = text; UpdateDirty(); return Error.Ok;
    }
    public void RequestClose() { if (Dirty) ClosePrompt.PopupCentered(new Vector2I(420, 180)); else ApproveClose(); }
    private void ApproveClose() { ApprovedCount++; if (QuitOnApproved) GetTree().Quit(); }
    public override void _ExitTree()
    {
        if (ReckfellObject.IsInstanceValid(_window)) _window.CloseRequested -= RequestClose;
        GetTree().AutoAcceptQuit = _autoAccept;
    }
}

Saving behavior

Programmatic TextEdit.Text assignment is not a user TextChanged. ReplaceText/loading therefore explicitly reconcile Dirty with the saved snapshot. A save error retains Dirty and keeps the dialog open. UI edits also arrive through TextChanged; button labels do not replace state checks.

The example writes a temporary file in the same directory, then File.Move with overwrite replaces the destination. Exceptions become Error. This does not guarantee power-loss durability or solve concurrent process writes. A failed write/replacement must not count as a successful save. See .NET File.Move.

The example caps text at one million UTF-16 units and loads at four million bytes. It keeps one document and does not monitor external file changes. A full editor also needs file selection, external-change conflicts, history/autosave and recovery policy; keep those separate from window state.

Windows and desktop integration

Window is a viewport with its own UI area. AcceptDialog/ConfirmationDialog inherit Window. Embedded subwindows and native windows are different modes; display/window/subwindows/embed_subwindows selects embedding, while Feature.Subwindows checks separate-window support. Handle secondary-window CloseRequested; hiding and QueueFree have different lifetimes. The main window governs process lifetime.

TaskAPI / check
Minimum/maximum dimensionsWindow.MinSize/MaxSize and real long translations
Native file selectionFileDialog.UseNativeDialog and Feature.NativeDialogFile; retain an embedded option when unsupported
Tray iconFeature.StatusIndicator, CreateStatusIndicator, DeleteStatusIndicator; clean up created IDs
Global menuNativeMenu and HasFeature/HasSystemMenu; system menus differ by platform
Client-side window decorationExtendToTitle, drag and system-button support in the backend
Task progressWindowSetTaskbarProgressState/Value; set state and a permitted value
NotificationsSendToastNotification; backend support, OS restrictions and user-initiated action
Transparent overlayPer-pixel transparency permission, transparent viewport, input/focus; transparent pixels do not imply mouse passthrough
Delayed first displayConfigure splash/first rendering and Show timing; avoid a half-built window
Screen readerFeature.AccessibilityScreenReader, roles/names/focus; test with real assistive technology

Headless returns dummy results for many DisplayServer calls and does not prove OS integration. An SDK method does not imply support in the current backend. Embedded editor runs do not replace exported-application checks.

Retain window geometry

WindowStateStore saves normal windowed mode only. Obtain the current DisplayServer.ScreenGetUsableRect for the intended screen before Restore. A removed monitor can leave old coordinates offscreen; Fit bounds position/size to the usable area, including monitors at negative coordinates. Embedded windows use their parent viewport area, so reconcile coordinate spaces before applying screen geometry to them.

WindowStateStore.cs
C#
using System;
using Reckfell;

public static class WindowStateStore
{
    public static Rect2I Fit(Rect2I requested, Rect2I usable)
    {
        if (usable.Size.X <= 0 || usable.Size.Y <= 0) throw new ArgumentOutOfRangeException(nameof(usable));
        int width = Math.Clamp(requested.Size.X, Math.Min(320, usable.Size.X), usable.Size.X);
        int height = Math.Clamp(requested.Size.Y, Math.Min(240, usable.Size.Y), usable.Size.Y);
        int x = Math.Clamp(requested.Position.X, usable.Position.X, usable.End.X - width);
        int y = Math.Clamp(requested.Position.Y, usable.Position.Y, usable.End.Y - height);
        return new Rect2I(x, y, width, height);
    }
    public static Error Save(Window window, string path = "user://window-state.cfg")
    {
        if (window.Mode != Window.ModeEnum.Windowed) return Error.Busy;
        var cfg = new ConfigFile(); cfg.SetValue("window", "position", window.Position); cfg.SetValue("window", "size", window.Size);
        return cfg.Save(path);
    }
    public static Error Restore(Window window, Rect2I usable, string path = "user://window-state.cfg")
    {
        var cfg = new ConfigFile(); Error error = cfg.Load(path); if (error != Error.Ok) return error;
        if (!cfg.HasSectionKey("window", "position") || !cfg.HasSectionKey("window", "size")) return Error.InvalidData;
        Variant position = cfg.GetValue("window", "position"), size = cfg.GetValue("window", "size");
        if (position.VariantType != Variant.Type.Vector2I || size.VariantType != Variant.Type.Vector2I) return Error.InvalidData;
        if (usable.Size.X <= 0 || usable.Size.Y <= 0) return Error.Unavailable;
        Rect2I fitted = Fit(new Rect2I(position.AsVector2I(), size.AsVector2I()), usable);
        window.Size = fitted.Size; window.Position = fitted.Position; return Error.Ok;
    }
}

Settings and distribution

For a simple tool, configure its main level, title/icons, HiDPI and window bounds. Distinguish content dimensions, rendered scaling and DPI. application/run/low_processor_mode can reduce idle work; continuous animation, _Process and redraw affect results. Test used features before removing build subsystems. Mobile tools also need safe-area, virtual-keyboard, lifecycle and system-back handling.

Export with the matching templates and .NET dependencies. A single executable, portable directory, installer and signing are distinct release tasks; verify the artifact before promising a standalone exe. User data usually remains in user://; portable mode needs an explicit writable location. Installer updates must not remove user data with program files. Start with project export.

Separate document logic, filesystem failures, UI/focus and actual device/export tests. This example uses a dedicated data directory, embedded windows, close signals and a separate process that actually quits. It does not certify native titlebars, notifications, tray integration or an installer.

Reckfell Engine documentationBack to top ↑
QUICK LINKSGetting startedEntity hierarchyC# & .NET 10Scenes and prefabs
Reckfell guides and full API K
reckfellENGINE
RECKFELL ACCOUNT

Sign in

Sign in to your Reckfell account.

One account for the entire Reckfell platform.