DVUI

init

It's expected to call this when self is undefined

Parameters

#
self:*TextEntryWidget
*TextEntryWidget
src:std.builtin.SourceLocation
std.builtin.SourceLocation
init_opts:InitOptions
InitOptions
opts:Options
Options

Source

Implementation

#
pub fn init(self: *TextEntryWidget, src: std.builtin.SourceLocation, init_opts: InitOptions, opts: Options) void {
    var scroll_init_opts = ScrollAreaWidget.InitOpts{
        .vertical = if (init_opts.scroll_vertical orelse init_opts.multiline) .auto else .none,
        .vertical_bar = init_opts.scroll_vertical_bar orelse .auto,
        .horizontal = if (init_opts.scroll_horizontal orelse true) .auto else .none,
        .horizontal_bar = init_opts.scroll_horizontal_bar orelse (if (init_opts.multiline) .auto else .hide),
    };

    var options = defaults.min_sizeM(defaultMWidth, 1);

    if (init_opts.password_char != null) {
        options.role = .password_input;
    } else if (init_opts.multiline) {
        options.role = .multiline_text_input;
    }

    options = options.override(opts);
    if (!init_opts.multiline and options.max_size_content == null) {
        options = options.override(.{ .max_size_content = .size(options.min_size_contentGet()) });
    }

    // padding is interpreted as the padding for the TextLayoutWidget, but
    // we also need to add it to content size because TextLayoutWidget is
    // inside the scroll area
    const padding = options.paddingGet();
    options.padding = null;
    options.min_size_content.?.w += padding.x + padding.w;
    options.min_size_content.?.h += padding.y + padding.h;
    if (options.max_size_content != null) {
        options.max_size_content.?.w += padding.x + padding.w;
        options.max_size_content.?.h += padding.y + padding.h;
    }

    const wd = WidgetData.init(src, .{}, options);
    scroll_init_opts.focus_id = wd.id;

    var text: []u8 = undefined;
    var find_zero = true;
    var len_utf8_boundary: usize = undefined;
    switch (init_opts.text) {
        .buffer => |b| text = b,
        .buffer_dynamic => |b| text = b.backing.*,
        .internal => text = dvui.dataGetSliceDefault(null, wd.id, "_buffer", []u8, &.{}),
        .array_list => |al| {
            find_zero = false;
            text = al.backing.items.ptr[0..@min(al.limit, al.backing.capacity)];
            len_utf8_boundary = dvui.findUtf8Start(text, al.backing.items.len);
        },
    }

    if (find_zero) {
        const len_byte = std.mem.findScalar(u8, text, 0) orelse text.len;
        len_utf8_boundary = dvui.findUtf8Start(text[0..len_byte], len_byte);
    }

    self.* = .{
        .wd = wd,
        .padding = padding,
        .init_opts = init_opts,
        .text = text,
        .len = len_utf8_boundary,

        // SAFETY: The following fields are set bellow
        .prevClip = undefined,
        .scroll = undefined,
        .scrollClip = undefined,
        .textLayout = undefined,
        .textClip = undefined,
    };

    self.data().register();

    dvui.tabIndexSet(self.data().id, self.data().options.tab_index, self.data().rectScale().r);

    dvui.parentSet(self.widget());

    self.data().borderAndBackground(.{});

    self.prevClip = dvui.clip(self.data().borderRectScale().r);
    const borderClip = dvui.clipGet();

    // We do this dance with last_focused_id_this_frame so scroll will process
    // key events we skip (like page up/down). Normally it would not (text
    // entry is not a child of scroll). So with this we make scroll think that
    // text entry ran as a child.
    const focused = (self.data().id == dvui.lastFocusedIdInFrame());
    if (focused) dvui.currentWindow().last_focused_id_this_frame = .zero;

    // scrollbars process mouse events here
    self.scroll.init(@src(), scroll_init_opts, self.data().options.strip().override(.{ .role = .none, .expand = .both }));

    if (focused) dvui.currentWindow().last_focused_id_this_frame = self.data().id;

    self.scrollClip = dvui.clipGet();

    self.edited_outside_last_frame = dvui.dataGetPtrDefault(null, self.data().id, "_edited_outside", bool, false);
    if (self.init_opts.cache_layout and self.edited_outside_last_frame.*) {
        dvui.log.debug("TextEntryWidget forcing cache_layout false due to text being edited after drawing last frame", .{});
        self.init_opts.cache_layout = false;
        self.edited_outside_last_frame.* = false;
        self.text_changed = true; // trigger tree_sitter full reparse
    }

    self.textLayout.init(@src(), .{
        .break_lines = self.init_opts.break_lines,
        .kerning = self.init_opts.kerning,
        .touch_edit_just_focused = false,
        .cache_layout = self.init_opts.cache_layout,
        .focused = self.data().id == dvui.focusedWidgetId(),
        .show_touch_draggables = (self.len > 0),
    }, self.data().options.strip().override(.{
        .role = .none,
        .expand = .both,
        .padding = self.padding,
    }));

    // if textLayout forced cache_layout to false, we need to honor that
    self.init_opts.cache_layout = self.textLayout.cache_layout;

    self.textClip = dvui.clipGet();

    if (self.textLayout.touchEditing()) |floating_widget| {
        defer floating_widget.deinit();

        var hbox = dvui.box(@src(), .{ .dir = .horizontal }, .{
            .corners = dvui.ButtonWidget.defaults.cornersGet(),
            .background = true,
            .border = dvui.Rect.all(1),
        });
        defer hbox.deinit();

        if (dvui.buttonIcon(@src(), "paste", dvui.entypo.clipboard, .{}, .{}, .{
            .min_size_content = .{ .h = 20 },
            .margin = Rect.all(2),
        })) {
            self.paste();
        }

        if (dvui.buttonIcon(@src(), "select all", dvui.entypo.swap, .{}, .{}, .{
            .min_size_content = .{ .h = 20 },
            .margin = Rect.all(2),
        })) {
            self.textLayout.selection.selectAll();
        }

        if (dvui.buttonIcon(@src(), "cut", dvui.entypo.scissors, .{}, .{}, .{
            .min_size_content = .{ .h = 20 },
            .margin = Rect.all(2),
        })) {
            self.cut();
        }

        if (dvui.buttonIcon(@src(), "copy", dvui.entypo.copy, .{}, .{}, .{
            .min_size_content = .{ .h = 20 },
            .margin = Rect.all(2),
        })) {
            self.copy();
        }
    }

    // don't call textLayout.processEvents here, we forward events inside our own processEvents

    // textLayout is maintaining the selection for us, but if the text
    // changed, we need to update the selection to be valid before we
    // process any events
    var sel = self.textLayout.selection;
    sel.start = dvui.findUtf8Start(self.text[0..self.len], sel.start);
    sel.cursor = dvui.findUtf8Start(self.text[0..self.len], sel.cursor);
    sel.end = dvui.findUtf8Start(self.text[0..self.len], sel.end);

    // textLayout clips to its content, but we need to get events out to our border
    dvui.clipSet(borderClip);
    if (self.data().accesskit_node()) |ak_node| {
        AccessKit.nodeAddAction(ak_node, AccessKit.Action.focus);
        AccessKit.nodeAddAction(ak_node, AccessKit.Action.set_value);
        AccessKit.nodeAddAction(ak_node, AccessKit.Action.set_text_selection);
        AccessKit.nodeAddAction(ak_node, AccessKit.Action.replace_selected_text);
        AccessKit.nodeAddAction(ak_node, AccessKit.Action.scroll_into_view); // AK TODO - not yet implemented
        AccessKit.nodeSetClipsChildren(ak_node); // AK TODO: Check this is correct?

        if (self.data().options.role != .password_input) {
            const str = self.text[0..self.len];
            AccessKit.nodeSetValueWithLength(ak_node, str.ptr, str.len);
        }
    }
}