DVUI

textEntryNumber

Parameters

#
src:std.builtin.SourceLocation
std.builtin.SourceLocation
T:type
type
init_opts:TextEntryNumberInitOptions(T)
TextEntryNumberInitOptions(T)
opts:Options
Options

Source

Implementation

#
pub fn textEntryNumber(src: std.builtin.SourceLocation, comptime T: type, init_opts: TextEntryNumberInitOptions(T), opts: Options) TextEntryNumberResult(T) {
    const default_opts: Options = .{
        .role = .number_input,
    };
    const base_filter = "1234567890";
    const filter = switch (@typeInfo(T)) {
        .int => |int| switch (int.signedness) {
            .signed => base_filter ++ "+-",
            .unsigned => base_filter ++ "+",
        },
        .float => base_filter ++ "+-.e",
        else => unreachable,
    };

    // @typeName is needed so that the id changes with the type for `data...` functions
    // https://github.com/david-vanderson/dvui/issues/502
    const id = dvui.parentGet().extendId(src, opts.idExtra()).update(@typeName(T));
    const backing_buffer: [30]u8 = @splat(0);
    const text_limit = init_opts.text_limit orelse 30;

    const buffer = dataGetSliceDefault(null, id, "_buffer", []u8, &backing_buffer)[0..@min(text_limit, backing_buffer.len)];

    // always initialize with value so we do the dataGet
    if (init_opts.value) |num| {
        const old_value = dataGet(null, id, "value", T);
        if (old_value == null or old_value.? != num.*) {
            dataSet(null, id, "value", num.*);
            @memset(buffer, 0); // clear out anything that was there before
            _ = std.fmt.bufPrint(buffer, "{d}", .{num.*}) catch unreachable;
        }
    }

    // display min/max
    var minmax_buffer: [64]u8 = undefined;
    var minmax_text: []const u8 = "";
    if (init_opts.show_min_max) {
        if (init_opts.min != null and init_opts.max != null) {
            minmax_text = std.fmt.bufPrint(&minmax_buffer, "(min: {d}, max: {d})", .{ init_opts.min.?, init_opts.max.? }) catch unreachable;
        } else if (init_opts.min != null) {
            minmax_text = std.fmt.bufPrint(&minmax_buffer, "(min: {d})", .{init_opts.min.?}) catch unreachable;
        } else if (init_opts.max != null) {
            minmax_text = std.fmt.bufPrint(&minmax_buffer, "(max: {d})", .{init_opts.max.?}) catch unreachable;
        }
    }

    const font = default_opts.override(opts).fontGet();
    var limit_size: ?Size = null;
    if (init_opts.text_limit) |limit| {
        limit_size = font.sizeM(limit, 1);
    }
    const options = default_opts.override(.{ .min_size_content = limit_size }).override(opts);

    var te: TextEntryWidget = undefined;
    te.init(src, .{
        .text = .{ .buffer = buffer },
        .placeholder = init_opts.placeholder orelse if (init_opts.show_min_max) minmax_text else null,
    }, options);

    // if text was given, act like the user deleted everything and typed this
    if (init_opts.text) |text| {
        te.textSet(text, false);
    }

    te.processEvents();

    // filter before drawing
    te.filterIn(filter);

    var result: TextEntryNumberResult(T) = .{ .enter_pressed = te.enter_pressed };

    // validation
    const text = te.getText();
    const num = switch (@typeInfo(T)) {
        .int => std.fmt.parseInt(T, text, 10) catch null,
        .float => std.fmt.parseFloat(T, text) catch null,
        else => unreachable,
    };

    //determine error if any
    if (text.len == 0 and num == null) {
        result.value = .Empty;
    } else if (num == null) {
        result.value = .Invalid;
    } else if (num != null and init_opts.min != null and num.? < init_opts.min.?) {
        result.value = .TooSmall;
    } else if (num != null and init_opts.max != null and num.? > init_opts.max.?) {
        result.value = .TooBig;
    } else {
        result.value = .{ .Valid = num.? };
        if (init_opts.value) |value_ptr| {
            if ((te.enter_pressed or te.text_changed) and value_ptr.* != num.?) {
                dataSet(null, id, "value", num.?);
                value_ptr.* = num.?;
                result.changed = true;
            }
        }
    }

    te.draw();

    if (result.value != .Valid and (init_opts.value != null or result.value != .Empty)) {
        const rs = te.data().borderRectScale();
        rs.r.outsetAll(1).stroke(te.data().options.cornersGet().scale(rs.s, CornerRect.Physical), .{ .thickness = 3 * rs.s, .color = dvui.themeGet().err.fill orelse .red, .after = true });
    }

    if (te.data().accesskit_node()) |ak_node| {
        AccessKit.nodeClearValue(ak_node); // Only set a numberic value
        if (@typeInfo(T) == .float) {
            if (init_opts.min) |min| AccessKit.nodeSetMinNumericValue(ak_node, @floatCast(min));
            if (init_opts.max) |max| AccessKit.nodeSetMinNumericValue(ak_node, @floatCast(max));
            if (num) |value|
                AccessKit.nodeSetNumericValue(ak_node, @floatCast(value));
        } else {
            if (init_opts.min) |min| AccessKit.nodeSetMinNumericValue(ak_node, @floatFromInt(min));
            if (init_opts.max) |max| AccessKit.nodeSetMinNumericValue(ak_node, @floatFromInt(max));
            if (num) |value|
                AccessKit.nodeSetNumericValue(ak_node, @floatFromInt(value));
        }
        if (result.value == .Valid) {
            AccessKit.nodeClearInvalid(ak_node);
        } else {
            AccessKit.nodeSetInvalid(ak_node, AccessKit.Invalid.ak_true);
            AccessKit.nodeSetNumericValue(ak_node, 0);
        }
    }

    te.deinit();

    return result;
}