DVUI

addTextDone

Parameters

#
self:*TextLayoutWidget
*TextLayoutWidget
opts:Options
Options

Source

Implementation

#
pub fn addTextDone(self: *TextLayoutWidget, opts: Options) void {
    if (self.add_text_done) {
        dvui.log.debug("TextLayoutWidget {x} addTextDone() called multiple times", .{self.data().id});
    }

    self.add_text_done = true;

    self.checkAscent();

    if (self.cache_layout and self.byte_heights.len > 0) {
        var edit_height: f32 = undefined;
        if (self.byte_height_after_idx) |i| {
            // this is not the final one
            const bh = self.byte_heights[i];

            // we expected to end at bh.height without edits, this is the extra
            // height the edits gave (might be negative)
            edit_height = self.insert_pt.y - bh.height;
            const edit_bytes: i64 = @as(i64, @intCast(self.bytes_seen)) - @as(i64, @intCast(bh.byte));
            const edit_lines: i64 = @as(i64, @intCast(self.line)) - @as(i64, @intCast(bh.line));

            // these are the height and bytes we are skipping
            const extra_height = self.byte_heights[self.byte_heights.len - 1].height - bh.height;
            const extra_bytes = self.byte_heights[self.byte_heights.len - 1].byte - bh.byte;
            self.bytes_seen += extra_bytes;

            // set min height
            const end_size = self.data().options.padSize(.{ .h = self.insert_pt.y + extra_height });
            self.data().min_size.h = @max(self.data().min_size.h, end_size.h);

            // adjust for edits
            for (self.byte_heights[i..self.byte_heights.len]) |*bhh| {
                bhh.height += edit_height;
                if (edit_bytes >= 0) {
                    bhh.byte += @intCast(edit_bytes);
                } else {
                    bhh.byte -= @intCast(-edit_bytes);
                }
                if (edit_lines >= 0) {
                    bhh.line += @intCast(edit_lines);
                } else {
                    bhh.line -= @intCast(-edit_lines);
                }
            }

            // copy all the ByteHeights we skipped, but not the final one
            self.byte_heights_new.appendSlice(dvui.currentWindow().arena(), self.byte_heights[i .. self.byte_heights.len - 1]) catch {};

            var k: usize = self.line_ascents_idx;
            while (k < self.line_ascents.len and self.line_ascents[k].line < self.line) k += 1;
            for (self.line_ascents[k..self.line_ascents.len]) |*la| {
                if (edit_lines >= 0) {
                    la.line += @intCast(edit_lines);
                } else {
                    la.line -= @intCast(-edit_lines);
                }
            }
            self.line_ascents_new.appendSlice(dvui.currentWindow().arena(), self.line_ascents[k..self.line_ascents.len]) catch {};
        } else {
            // use the final one
            var bh = &self.byte_heights[self.byte_heights.len - 1];

            // we expected to end at bh.height without edits, this is the extra
            // height the edits gave (might be negative)
            const os = self.data().options;
            const contentMinSize = self.data().min_size.padNeg(os.paddingGet()).padNeg(os.borderGet()).padNeg(os.marginGet());
            edit_height = contentMinSize.h - bh.height;

            // adjust previous height for sanity check below
            bh.height += edit_height;
        }

        std.debug.assert(self.cache_layout_bytes_seen == self.bytes_seen);
        //std.debug.print("edit_height {d}\n", .{edit_height});

        // TODO: if edit_height is negative, we might not render some text for a frame - need to scan further in byte_heights until we find one that is not visible
    }

    const os = self.data().options;
    const contentMinSize = self.data().min_size.padNeg(os.paddingGet()).padNeg(os.borderGet()).padNeg(os.marginGet());
    self.byte_heights_new.append(dvui.currentWindow().arena(), .{ .byte = self.bytes_seen, .height = contentMinSize.h, .line = self.line }) catch {};

    if (self.cache_layout and self.byte_heights.len > 0) {
        // sanity check
        const old = self.byte_heights[self.byte_heights.len - 1].height;
        const new = self.byte_heights_new.items[self.byte_heights_new.items.len - 1].height;
        if (new < (old - 1.0) or new > (old + 1.0)) {
            dvui.logError(@src(), error.CacheLayoutError, "the height of the processed text changed by {d}, cache_layout should have been false this frame", .{new - old});
            self.byte_heights_new.clearAndFree(dvui.currentWindow().arena());
        }
    }
    //std.debug.print("final height: {d} at {d}\n", .{ contentMinSize.h, self.bytes_seen });

    //const crs = self.data().contentRectScale();
    //for (self.byte_heights_new.items) |bhn| {
    //    //std.debug.print("bh: {d} - {d}\n", .{ bhn.byte, bhn.height });
    //    const p: dvui.Path = .{ .points = &.{
    //        crs.pointToPhysical(.{ .x = 0, .y = bhn.height }),
    //        crs.pointToPhysical(.{ .x = 100, .y = bhn.height }),
    //    } };
    //    p.stroke(.{ .thickness = 1, .color = .red });
    //}

    self.selection.cursor = @min(self.selection.cursor, self.bytes_seen);
    self.selection.start = @min(self.selection.start, self.bytes_seen);
    self.selection.end = @min(self.selection.end, self.bytes_seen);

    const options = self.data().options.override(opts);
    const text_height = options.fontGet().textHeight();

    if (!self.cursor_seen) {
        self.cursor_rect = Rect{ .x = self.insert_pt.x, .y = self.insert_pt.y, .w = 1, .h = text_height };
        self.cursorSeen();
    }

    if (self.copy_sel) |_| {
        // we are copying to clipboard and never stopped
        dvui.clipboardTextSet(self.copy_slice orelse "");

        self.copy_sel = null;
        if (self.copy_slice) |cs| {
            dvui.currentWindow().arena().free(cs);
        }
        self.copy_slice = null;
    }

    // handle selection movement
    // - this logic must work even if addText() is never called
    switch (self.sel_move) {
        .none => {},
        .mouse => |*m| {
            if (m.down_pt) |_| {
                m.byte = self.bytes_seen;
                self.selection.moveCursor(self.bytes_seen, false);
                m.down_pt = null;
            }

            if (m.drag_pt) |_| {
                self.selection.cursor = self.bytes_seen;
                self.selection.start = @min(m.byte.?, self.bytes_seen);
                self.selection.end = @max(m.byte.?, self.bytes_seen);
                m.drag_pt = null;
            }
        },
        .expand_pt => |*ep| {
            if (!ep.done and !ep.select) {
                self.selection.moveCursor(self.selection.cursor, false);
            }
        },
        .char_left_right => {},
        .cursor_updown => |*cud| {
            if (cud.pt) |_| {
                self.selection.moveCursor(self.bytes_seen, cud.select);
                cud.pt = null;
            }
        },
        .word_left_right => {},
    }

    if (self.sel_start_r_new) |start_r| {
        if (!self.sel_start_r.equals(start_r)) {
            dvui.refresh(null, @src(), self.data().id);
        }
        self.sel_start_r = start_r;
    }

    if (self.selection.start > self.bytes_seen or self.bytes_seen == 0) {
        self.sel_start_r = .{ .x = self.insert_pt.x, .y = self.insert_pt.y, .w = 1, .h = text_height };
        if (self.selection.start > self.bytes_seen) {
            dvui.refresh(null, @src(), self.data().id);
        }
    }

    if (self.sel_end_r_new) |end_r| {
        if (!self.sel_end_r.equals(end_r)) {
            dvui.refresh(null, @src(), self.data().id);
        }
        self.sel_end_r = end_r;
    }

    if (self.selection.end > self.bytes_seen or self.bytes_seen == 0) {
        self.sel_end_r = .{ .x = self.insert_pt.x, .y = self.insert_pt.y, .w = 1, .h = text_height };
        if (self.selection.end > self.bytes_seen) {
            dvui.refresh(null, @src(), self.data().id);
        }
    }

    const cw = dvui.currentWindow();
    if (dvui.accesskit_enabled) if (cw.accesskit.text_run_parent) |text_run_parent| {
        if (cw.accesskit.nodes.get(text_run_parent)) |parent_node| {
            if (self.bytes_seen == 0 or self.newline) {
                // No empty text run was created as no text was rendered. Create one here.
                self.textRunCreateEmpty(if (self.data().options.role.? == .none) cw.accesskit.text_run_parent.? else self.data().id, self.bytes_seen == 0);
            }

            if (!self.selection.empty()) {
                self.textrun_anchor = self.textrun_anchor orelse self.textrun_last;
                self.textrun_focus = self.textrun_focus orelse self.textrun_last;

                if (self.textrun_anchor) |anchor| {
                    if (self.textrun_focus) |focus| {
                        AccessKit.nodeSetTextSelection(parent_node, .{
                            .anchor = .{ .node = anchor.node_id.asU64(), .character_index = anchor.pos },
                            .focus = .{ .node = focus.node_id.asU64(), .character_index = focus.pos },
                        });
                    }
                }
            } else {
                // if we didn't find the cursor, it must be at the end
                if (self.textrun_cursor orelse self.textrun_last) |cursor| {
                    AccessKit.nodeSetTextSelection(parent_node, .{
                        .anchor = .{ .node = cursor.node_id.asU64(), .character_index = cursor.pos },
                        .focus = .{ .node = cursor.node_id.asU64(), .character_index = cursor.pos },
                    });
                }
            }
        }
    };
}