Goose

29_ui_todo.goose

// A small to-do list, and a color mixer for what lies behind it, built with
// the ui module (docs/stdlib.md): Nuklear's immediate-mode widgets, drawn
// through gfx.
//
// Immediate mode means each frame the program says which windows and
// widgets there are, in order, and learns what the user did to them from
// the same calls: a button's returns true on the frame it is clicked, a
// check box flips the bool it is given, a text field edits the text it is
// given. The program keeps its own data as it likes; the ui keeps only
// where the windows are and what is open or active.
//
// Type a task and press Enter or Add, tick tasks off, pick which to show,
// mix the backdrop, and make the whole ui bigger or smaller. Escape or
// closing the window quits. `-- --frames N` stops after N frames. Either
// way the last frame is saved as ui_todo.png.
//
//   goose 29_ui_todo.goose
//
// Built from the generated C, the program links the ui layer and gfx's,
// whose paths `goose --ui-link` and `goose --gfx-link` print:
//   goose -o ui_todo.c 29_ui_todo.goose
//   cl ui_todo.c @<goose --ui-link msvc> @<goose --gfx-link msvc>
//   cc ui_todo.c -o ui_todo @<goose --ui-link cc> @<goose --gfx-link cc>

import std;
import vec;
import math;
import os;
import gfx;
import ui;

// Roboto (Apache 2.0, one of Nuklear's extra fonts), read into the program
// when it is compiled: no file to ship beside the executable.
let ROBOTO: const u8[:] = embed_file("data/Roboto-Regular.ttf");

struct Task { text: u8[..40], done: bool }

fn task(text: const u8[:], done: bool) -> Task {
    var t = Task { text: [], done: done };
    t.text.append(text);
    t
}

let SHOW_ALL = 0;
let SHOW_TO_DO = 1;
let SHOW_DONE = 2;

fn main() {
    var frames = -1;
    let argv = args();
    if argv.len == 3 && argv[1] == "--frames" {
        let n, ok = parse_int(argv[2]);
        if ok { frames = n; }
    }
    guard gfx::open("Goose: a ui to-do list", 760, 520, gfx::WINDOW_RESIZABLE) else {
        write_stderr(str("gfx: ", gfx::error(), "\n"));
        return;
    }
    // A context holds the ui's state; this one draws with the embedded
    // Roboto, 16 pixels high at a scale of 1 and baked again whenever the
    // scale changes.
    var ctx = ui::create_from_memory(ROBOTO, 16.0);
    guard ctx.id != 0 else {
        write_stderr(str("ui: ", ui::error(), "\n"));
        return;
    }
    // Its style is Nuklear's, but for a brighter mark in ticked check boxes
    // and chosen options.
    var colors = ui::default_color_table();
    colors[ui::COLOR_TOGGLE_CURSOR] = ui::rgb(120, 200, 140);
    ui::style_from_table(ctx, colors);

    var tasks: Task[>..] = [];
    tasks.push(task("Feed the goose", true));
    tasks.push(task("Read docs/stdlib.md", false));
    tasks.push(task("Draw a window with ui", false));
    var entry: u8[..40] = [];
    var show = SHOW_ALL;
    var backdrop = ui::Colorf { 0.18, 0.24, 0.30, 1.0 };
    var next_scale = ui::scale(ctx);
    let window = ui::WINDOW_BORDER | ui::WINDOW_MOVABLE | ui::WINDOW_SCALABLE |
                 ui::WINDOW_TITLE | ui::WINDOW_MINIMIZABLE;
    var drawn = 0;

    while gfx::frame() {
        if gfx::key_pressed("Escape") { break; }
        // The frame's keys, mouse and typing, from gfx.
        ui::input_from_gfx(ctx);
        var quit = false;

        if ui::begin(ctx, "To do", ui::rect(30, 30, 380, 365), window) {
            // A menu bar goes first in its window.
            ui::menubar_begin(ctx);
            ui::layout_row_static(ctx, 20.0, 60, 1);
            if ui::menu_begin_label(ctx, "Tasks", ui::TEXT_LEFT, float2 { 140.0, 100.0 }) {
                ui::layout_row_dynamic(ctx, 22.0, 1);
                if ui::menu_item_label(ctx, "Tick them all", ui::TEXT_LEFT) {
                    for i in tasks.len { tasks[i].done = true; }
                }
                if ui::menu_item_label(ctx, "Clear the done", ui::TEXT_LEFT) {
                    var i = tasks.len;
                    while i > 0 {
                        i--;
                        if tasks[i].done { remove_at(tasks, i); }
                    }
                }
                if ui::menu_item_label(ctx, "Quit", ui::TEXT_LEFT) { quit = true; }
                ui::menu_end(ctx);
            }
            ui::menubar_end(ctx);

            // The text field takes three quarters of the row, the button
            // the rest. Enter commits the field as the button does.
            let ratios: f32[2] = [0.75, 0.25];
            ui::layout_row(ctx, ui::DYNAMIC, 28.0, ratios);
            let edited = ui::edit_string(ctx, ui::EDIT_FIELD | ui::EDIT_SIG_ENTER, entry,
                                         ui::FILTER_DEFAULT);
            // A tooltip is asked for before the widget it is about.
            if ui::widget_is_hovered(ctx) { ui::tooltip(ctx, "Adds the task (Enter does too)"); }
            let add = ui::button_label(ctx, "Add");
            if (add || (edited & ui::EDIT_COMMITTED) != 0) && entry.len > 0 {
                tasks.push(Task { text: entry, done: false });
                entry.resize(0);
            }

            // Which tasks show: one of three.
            ui::layout_row_dynamic(ctx, 24.0, 3);
            if ui::option_label(ctx, "All", show == SHOW_ALL) { show = SHOW_ALL; }
            if ui::option_label(ctx, "To do", show == SHOW_TO_DO) { show = SHOW_TO_DO; }
            if ui::option_label(ctx, "Done", show == SHOW_DONE) { show = SHOW_DONE; }

            // A check box per task, with a button that removes it.
            let row: f32[2] = [0.88, 0.12];
            var remove = -1;
            for i in tasks.len {
                let done = tasks[i].done;
                if (show == SHOW_TO_DO && done) || (show == SHOW_DONE && !done) { continue; }
                ui::layout_row(ctx, ui::DYNAMIC, 24.0, row);
                let text = tasks[i].text;
                ui::checkbox_label(ctx, text, tasks[i].done);
                if ui::button_symbol(ctx, ui::SYMBOL_X) { remove = i; }
            }
            if remove >= 0 { remove_at(tasks, remove); }

            var finished = 0;
            for t in tasks { if t.done { finished++; } }
            ui::layout_row_dynamic(ctx, 22.0, 1);
            ui::label(ctx, str(finished, " of ", tasks.len, " done"), ui::TEXT_LEFT);
            ui::progress(ctx, finished, max(tasks.len, 1), false);
        }
        ui::end(ctx);

        if ui::begin(ctx, "Backdrop", ui::rect(440, 30, 290, 365), window) {
            ui::layout_row_dynamic(ctx, 20.0, 1);
            ui::label(ctx, "The color behind the windows:", ui::TEXT_LEFT);
            ui::layout_row_dynamic(ctx, 150.0, 1);
            backdrop = ui::color_picker(ctx, backdrop, ui::RGB);
            // Properties: drag them, click their arrows, or type a value.
            ui::layout_row_dynamic(ctx, 24.0, 1);
            ui::property_float(ctx, "#Red:", 0.0, backdrop.r, 1.0, 0.01, 0.005);
            ui::property_float(ctx, "#Green:", 0.0, backdrop.g, 1.0, 0.01, 0.005);
            ui::property_float(ctx, "#Blue:", 0.0, backdrop.b, 1.0, 0.01, 0.005);
            // The whole ui bigger or smaller, from the next frame on.
            ui::layout_row_dynamic(ctx, 24.0, 3);
            ui::label(ctx, str("Scale ", ui::scale(ctx)), ui::TEXT_LEFT);
            if ui::button_symbol(ctx, ui::SYMBOL_MINUS) {
                next_scale = max(ui::scale(ctx) - 0.25, 0.5);
            }
            if ui::button_symbol(ctx, ui::SYMBOL_PLUS) {
                next_scale = min(ui::scale(ctx) + 0.25, 3.0);
            }
        }
        ui::end(ctx);

        // The backdrop, then the windows over it.
        gfx::begin_screen(float4 { backdrop.r, backdrop.g, backdrop.b, 1.0 });
        gfx::end_pass();
        ui::render(ctx);
        // Between frames: the context's font is baked again at the scale.
        if next_scale != ui::scale(ctx) { ui::set_scale(ctx, next_scale); }
        drawn++;
        if quit || drawn == frames { break; }
    }

    let saved = gfx::screenshot("ui_todo.png");
    var finished = 0;
    for t in tasks { if t.done { finished++; } }
    print("drew ", drawn, " frames; ", finished, " of ", tasks.len,
          " tasks done; saved ui_todo.png: ", saved);
    ui::destroy(ctx);
    gfx::close();
}